product performance grouped kpi improvements
This commit is contained in:
@@ -4,6 +4,7 @@ type ProductPerformanceRow struct {
|
|||||||
KpiDate string `json:"kpi_date"`
|
KpiDate string `json:"kpi_date"`
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
@@ -86,6 +87,7 @@ type ProductPerformanceGeneralRow struct {
|
|||||||
PeriodEnd string `json:"period_end"`
|
PeriodEnd string `json:"period_end"`
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
@@ -123,6 +125,7 @@ type ProductPerformanceGeneralRow struct {
|
|||||||
type ProductPerformanceOrderAnalysisRow struct {
|
type ProductPerformanceOrderAnalysisRow struct {
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
@@ -187,6 +190,7 @@ type ProductPerformanceOrderGroupRow struct {
|
|||||||
type ProductPerformanceOrderProductCustomerRow struct {
|
type ProductPerformanceOrderProductCustomerRow struct {
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
@@ -224,6 +228,7 @@ type ProductPerformanceOrderMarketDetailRow struct {
|
|||||||
DueDate string `json:"due_date"`
|
DueDate string `json:"due_date"`
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
@@ -306,6 +311,7 @@ type ProductPerformanceSalesBreakdownRow struct {
|
|||||||
Breakdown string `json:"breakdown"`
|
Breakdown string `json:"breakdown"`
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
|
|||||||
@@ -369,6 +369,9 @@ SET urun_ilk_grubu = '', updated_at = now()
|
|||||||
WHERE btrim(urun_ilk_grubu) = '-'
|
WHERE btrim(urun_ilk_grubu) = '-'
|
||||||
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) 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')`,
|
||||||
|
`
|
||||||
UPDATE mk_product_performance_sales_daily
|
UPDATE mk_product_performance_sales_daily
|
||||||
SET askili_yan = '', updated_at = now()
|
SET askili_yan = '', updated_at = now()
|
||||||
WHERE btrim(askili_yan) = '-'`,
|
WHERE btrim(askili_yan) = '-'`,
|
||||||
@@ -434,6 +437,18 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_price_dim (
|
|||||||
`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
|
||||||
`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
|
||||||
`
|
`
|
||||||
|
UPDATE mk_product_performance_price_dim
|
||||||
|
SET
|
||||||
|
cost_price_usd = GREATEST(cost_price_usd, base_price_usd),
|
||||||
|
base_price_usd = LEAST(cost_price_usd, base_price_usd),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE cost_price_usd > 0
|
||||||
|
AND base_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')`,
|
||||||
|
`
|
||||||
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||||||
kpi_date DATE NOT NULL,
|
kpi_date DATE NOT NULL,
|
||||||
product_code TEXT NOT NULL,
|
product_code TEXT NOT NULL,
|
||||||
@@ -501,6 +516,26 @@ SET urun_ilk_grubu = '', updated_at = now()
|
|||||||
WHERE btrim(urun_ilk_grubu) = '-'
|
WHERE btrim(urun_ilk_grubu) = '-'
|
||||||
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) 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')`,
|
||||||
|
`
|
||||||
|
UPDATE mk_product_performance_kpi_daily
|
||||||
|
SET
|
||||||
|
cost_price_usd = GREATEST(cost_price_usd, base_price_usd),
|
||||||
|
base_price_usd = LEAST(cost_price_usd, base_price_usd),
|
||||||
|
gross_profit_usd_90d = COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * GREATEST(cost_price_usd, base_price_usd)),
|
||||||
|
gross_profit_usd_180d = COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * GREATEST(cost_price_usd, base_price_usd)),
|
||||||
|
gross_margin_90d = CASE WHEN COALESCE(sales_usd_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * GREATEST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_90d,0) END,
|
||||||
|
gross_margin_180d = CASE WHEN COALESCE(sales_usd_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * GREATEST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_180d,0) END,
|
||||||
|
unit_profit_cost_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
|
||||||
|
unit_profit_cost_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
|
||||||
|
unit_profit_base_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - LEAST(cost_price_usd, base_price_usd) END,
|
||||||
|
unit_profit_base_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - LEAST(cost_price_usd, base_price_usd) END,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE cost_price_usd > 0
|
||||||
|
AND base_price_usd > 0
|
||||||
|
AND cost_price_usd < base_price_usd`,
|
||||||
|
`
|
||||||
UPDATE mk_product_performance_kpi_daily
|
UPDATE mk_product_performance_kpi_daily
|
||||||
SET askili_yan = '', updated_at = now()
|
SET askili_yan = '', updated_at = now()
|
||||||
WHERE btrim(askili_yan) = '-'`,
|
WHERE btrim(askili_yan) = '-'`,
|
||||||
@@ -719,6 +754,67 @@ func normalizeProductPerformanceVariantCodes(productCode, colorCode, yakaKodu *s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformanceColorDescriptions(ctx context.Context, colorCodes []string) map[string]string {
|
||||||
|
out := map[string]string{}
|
||||||
|
if db.MssqlDB == nil || len(colorCodes) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
codes := make([]string, 0, len(colorCodes))
|
||||||
|
for _, raw := range colorCodes {
|
||||||
|
code := normalizeProductPerformanceCode(raw)
|
||||||
|
if code == "" || seen[code] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[code] = true
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
const batchSize = 500
|
||||||
|
for start := 0; start < len(codes); start += batchSize {
|
||||||
|
end := start + batchSize
|
||||||
|
if end > len(codes) {
|
||||||
|
end = len(codes)
|
||||||
|
}
|
||||||
|
batch := codes[start:end]
|
||||||
|
placeholders := make([]string, len(batch))
|
||||||
|
args := make([]any, len(batch))
|
||||||
|
for i, code := range batch {
|
||||||
|
placeholders[i] = fmt.Sprintf("@p%d", i+1)
|
||||||
|
args[i] = code
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
UPPER(LTRIM(RTRIM(ColorCode))) AS color_code,
|
||||||
|
MAX(LTRIM(RTRIM(ISNULL(ColorDescription, '')))) AS color_description
|
||||||
|
FROM cdColorDesc WITH(NOLOCK)
|
||||||
|
WHERE LangCode = 'TR'
|
||||||
|
AND UPPER(LTRIM(RTRIM(ColorCode))) IN (%s)
|
||||||
|
GROUP BY UPPER(LTRIM(RTRIM(ColorCode)))
|
||||||
|
`, strings.Join(placeholders, ","))
|
||||||
|
rows, err := db.MssqlDB.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ProductPerformance] color descriptions failed: %v", err)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var code, desc string
|
||||||
|
if err := rows.Scan(&code, &desc); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
log.Printf("[ProductPerformance] color description scan failed: %v", err)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
out[normalizeProductPerformanceCode(code)] = strings.TrimSpace(desc)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
log.Printf("[ProductPerformance] color description rows failed: %v", err)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
|
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
|
||||||
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate, productPrefix)
|
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate, productPrefix)
|
||||||
@@ -984,6 +1080,7 @@ func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx, productPre
|
|||||||
code = normalizeProductPerformanceProductCode(code)
|
code = normalizeProductPerformanceProductCode(code)
|
||||||
askiliYan = cleanProductPerformanceOptionalAttr(askiliYan)
|
askiliYan = cleanProductPerformanceOptionalAttr(askiliYan)
|
||||||
urunIlkGrubu = cleanProductPerformanceFirstGroup(urunIlkGrubu)
|
urunIlkGrubu = cleanProductPerformanceFirstGroup(urunIlkGrubu)
|
||||||
|
cost, usd = normalizeProductPerformanceCostPair(cost, usd)
|
||||||
var lp any
|
var lp any
|
||||||
if lastPricing.Valid {
|
if lastPricing.Valid {
|
||||||
lp = lastPricing.Time
|
lp = lastPricing.Time
|
||||||
@@ -1063,7 +1160,7 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
|
|||||||
}
|
}
|
||||||
limit := f.Limit
|
limit := f.Limit
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 100
|
limit = 50000
|
||||||
} else if limit > 50000 {
|
} else if limit > 50000 {
|
||||||
limit = 50000
|
limit = 50000
|
||||||
}
|
}
|
||||||
@@ -1103,6 +1200,7 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
out := make([]models.ProductPerformanceRow, 0, limit)
|
out := make([]models.ProductPerformanceRow, 0, limit)
|
||||||
|
colorCodes := make([]string, 0, limit)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r models.ProductPerformanceRow
|
var r models.ProductPerformanceRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -1122,9 +1220,17 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
|
r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
|
||||||
|
colorCodes = append(colorCodes, r.ColorCode)
|
||||||
out = append(out, r)
|
out = append(out, r)
|
||||||
}
|
}
|
||||||
return out, total, rows.Err()
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
for i := range out {
|
||||||
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
|
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
|
||||||
@@ -1141,6 +1247,7 @@ KPI AS (
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
),
|
),
|
||||||
VariantStock AS (
|
VariantStock AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -1167,6 +1274,7 @@ Sales90 AS (
|
|||||||
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
|
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
|
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
||||||
@@ -1195,7 +1303,7 @@ func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 500
|
limit = 50000
|
||||||
} else if limit > 50000 {
|
} else if limit > 50000 {
|
||||||
limit = 50000
|
limit = 50000
|
||||||
}
|
}
|
||||||
@@ -1236,6 +1344,7 @@ SalesAgg AS (
|
|||||||
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
|
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
|
||||||
FROM mk_product_performance_sales_daily, Bounds
|
FROM mk_product_performance_sales_daily, Bounds
|
||||||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY product_code, color_code, yaka_kodu, market_key
|
GROUP BY product_code, color_code, yaka_kodu, market_key
|
||||||
),
|
),
|
||||||
StockAgg AS (
|
StockAgg AS (
|
||||||
@@ -1287,6 +1396,7 @@ Dim AS (
|
|||||||
1 AS src_rank
|
1 AS src_rank
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
product_code,
|
product_code,
|
||||||
@@ -1302,6 +1412,7 @@ Dim AS (
|
|||||||
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
||||||
2 AS src_rank
|
2 AS src_rank
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
|
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY product_code, color_code, yaka_kodu
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
) x
|
) x
|
||||||
ORDER BY product_code, color_code, yaka_kodu, src_rank
|
ORDER BY product_code, color_code, yaka_kodu, src_rank
|
||||||
@@ -1333,6 +1444,7 @@ StockOnly AS (
|
|||||||
AND d.color_code = s.color_code
|
AND d.color_code = s.color_code
|
||||||
AND d.yaka_kodu = s.yaka_kodu
|
AND d.yaka_kodu = s.yaka_kodu
|
||||||
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
||||||
|
AND upper(btrim(COALESCE(d.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM SalesAgg a
|
FROM SalesAgg a
|
||||||
@@ -1356,6 +1468,7 @@ Spread AS (
|
|||||||
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
|
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
|
||||||
FROM mk_product_performance_sales_daily, Bounds
|
FROM mk_product_performance_sales_daily, Bounds
|
||||||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY product_code, color_code, yaka_kodu
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
),
|
),
|
||||||
Scored AS (
|
Scored AS (
|
||||||
@@ -1463,6 +1576,7 @@ LIMIT $1
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
out := make([]models.ProductPerformanceGeneralRow, 0, limit)
|
out := make([]models.ProductPerformanceGeneralRow, 0, limit)
|
||||||
|
colorCodes := make([]string, 0, limit)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r models.ProductPerformanceGeneralRow
|
var r models.ProductPerformanceGeneralRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -1476,9 +1590,17 @@ LIMIT $1
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
|
r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
|
||||||
|
colorCodes = append(colorCodes, r.ColorCode)
|
||||||
out = append(out, r)
|
out = append(out, r)
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
for i := range out {
|
||||||
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
||||||
@@ -1489,7 +1611,7 @@ func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 500
|
limit = 50000
|
||||||
} else if limit > 50000 {
|
} else if limit > 50000 {
|
||||||
limit = 50000
|
limit = 50000
|
||||||
}
|
}
|
||||||
@@ -1691,6 +1813,7 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
|
|||||||
out := make([]models.ProductPerformanceOrderAnalysisRow, 0, limit)
|
out := make([]models.ProductPerformanceOrderAnalysisRow, 0, limit)
|
||||||
productCodes := make([]string, 0, limit)
|
productCodes := make([]string, 0, limit)
|
||||||
stockKeys := make([]string, 0, limit)
|
stockKeys := make([]string, 0, limit)
|
||||||
|
colorCodes := make([]string, 0, limit)
|
||||||
seenProducts := map[string]bool{}
|
seenProducts := map[string]bool{}
|
||||||
seenStockKeys := map[string]bool{}
|
seenStockKeys := map[string]bool{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
@@ -1704,6 +1827,7 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
|
|||||||
}
|
}
|
||||||
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
|
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
|
||||||
r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan)
|
r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan)
|
||||||
|
colorCodes = append(colorCodes, r.ColorCode)
|
||||||
out = append(out, r)
|
out = append(out, r)
|
||||||
if !seenProducts[r.ProductCode] {
|
if !seenProducts[r.ProductCode] {
|
||||||
seenProducts[r.ProductCode] = true
|
seenProducts[r.ProductCode] = true
|
||||||
@@ -1731,10 +1855,15 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
filtered := out[:0]
|
||||||
for i := range out {
|
for i := range out {
|
||||||
row := &out[i]
|
row := &out[i]
|
||||||
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
|
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
|
||||||
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||||
|
if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.TrimSpace(row.ItemDescription) == "" {
|
if strings.TrimSpace(row.ItemDescription) == "" {
|
||||||
row.ItemDescription = attr.itemDescription
|
row.ItemDescription = attr.itemDescription
|
||||||
}
|
}
|
||||||
@@ -1765,9 +1894,11 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
|
|||||||
if row.OrderUSD != 0 {
|
if row.OrderUSD != 0 {
|
||||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||||
}
|
}
|
||||||
|
row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
|
||||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderRecommendation(*row)
|
row.PerformanceBucket, row.Recommendation = productPerformanceOrderRecommendation(*row)
|
||||||
|
filtered = append(filtered, *row)
|
||||||
}
|
}
|
||||||
return out, nil
|
return filtered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceOrderGroupRow, error) {
|
func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceOrderGroupRow, error) {
|
||||||
@@ -1781,8 +1912,10 @@ func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdow
|
|||||||
if mode != "customer" {
|
if mode != "customer" {
|
||||||
mode = "market"
|
mode = "market"
|
||||||
}
|
}
|
||||||
if limit <= 0 || limit > 1000 {
|
if limit <= 0 {
|
||||||
limit = 500
|
limit = 50000
|
||||||
|
} else if limit > 50000 {
|
||||||
|
limit = 50000
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||||
@@ -2015,8 +2148,10 @@ func ListProductPerformanceOrderProductCustomers(ctx context.Context, pg *sql.DB
|
|||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 || limit > 1000 {
|
if limit <= 0 {
|
||||||
limit = 500
|
limit = 50000
|
||||||
|
} else if limit > 50000 {
|
||||||
|
limit = 50000
|
||||||
}
|
}
|
||||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||||
WITH OpenOrderLines AS (
|
WITH OpenOrderLines AS (
|
||||||
@@ -2074,6 +2209,7 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
|||||||
out := make([]models.ProductPerformanceOrderProductCustomerRow, 0, limit)
|
out := make([]models.ProductPerformanceOrderProductCustomerRow, 0, limit)
|
||||||
productCodes := make([]string, 0, limit)
|
productCodes := make([]string, 0, limit)
|
||||||
stockKeys := make([]string, 0, limit)
|
stockKeys := make([]string, 0, limit)
|
||||||
|
colorCodes := make([]string, 0, limit)
|
||||||
seenProducts := map[string]bool{}
|
seenProducts := map[string]bool{}
|
||||||
seenStockKeys := map[string]bool{}
|
seenStockKeys := map[string]bool{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
@@ -2082,6 +2218,7 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
|
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
|
||||||
|
colorCodes = append(colorCodes, r.ColorCode)
|
||||||
out = append(out, r)
|
out = append(out, r)
|
||||||
if !seenProducts[r.ProductCode] {
|
if !seenProducts[r.ProductCode] {
|
||||||
seenProducts[r.ProductCode] = true
|
seenProducts[r.ProductCode] = true
|
||||||
@@ -2108,10 +2245,15 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
filtered := out[:0]
|
||||||
for i := range out {
|
for i := range out {
|
||||||
row := &out[i]
|
row := &out[i]
|
||||||
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
|
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
|
||||||
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||||
|
if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.TrimSpace(row.ItemDescription) == "" {
|
if strings.TrimSpace(row.ItemDescription) == "" {
|
||||||
row.ItemDescription = attr.itemDescription
|
row.ItemDescription = attr.itemDescription
|
||||||
}
|
}
|
||||||
@@ -2130,9 +2272,11 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
|||||||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||||
}
|
}
|
||||||
|
row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
|
||||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderProductCustomerRecommendation(*row)
|
row.PerformanceBucket, row.Recommendation = productPerformanceOrderProductCustomerRecommendation(*row)
|
||||||
|
filtered = append(filtered, *row)
|
||||||
}
|
}
|
||||||
return out, nil
|
return filtered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderMarketDetailRow, error) {
|
func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderMarketDetailRow, error) {
|
||||||
@@ -2199,6 +2343,7 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
|
|||||||
out := make([]models.ProductPerformanceOrderMarketDetailRow, 0, limit)
|
out := make([]models.ProductPerformanceOrderMarketDetailRow, 0, limit)
|
||||||
productCodes := make([]string, 0, limit)
|
productCodes := make([]string, 0, limit)
|
||||||
stockKeys := make([]string, 0, limit)
|
stockKeys := make([]string, 0, limit)
|
||||||
|
colorCodes := make([]string, 0, limit)
|
||||||
seenProducts := map[string]bool{}
|
seenProducts := map[string]bool{}
|
||||||
seenStockKeys := map[string]bool{}
|
seenStockKeys := map[string]bool{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
@@ -2207,6 +2352,7 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
|
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
|
||||||
|
colorCodes = append(colorCodes, r.ColorCode)
|
||||||
out = append(out, r)
|
out = append(out, r)
|
||||||
if !seenProducts[r.ProductCode] {
|
if !seenProducts[r.ProductCode] {
|
||||||
seenProducts[r.ProductCode] = true
|
seenProducts[r.ProductCode] = true
|
||||||
@@ -2229,9 +2375,19 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
attrByKey, err := productPerformanceAttrLookup(ctx, pg, stockKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
filtered := out[:0]
|
||||||
for i := range out {
|
for i := range out {
|
||||||
row := &out[i]
|
row := &out[i]
|
||||||
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
|
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
|
||||||
|
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||||
|
if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
row.CostPriceUSD = price.cost
|
row.CostPriceUSD = price.cost
|
||||||
row.BasePriceUSD = price.base
|
row.BasePriceUSD = price.base
|
||||||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||||
@@ -2242,17 +2398,21 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
|
|||||||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||||
}
|
}
|
||||||
|
row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
|
||||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderMarketDetailRecommendation(*row)
|
row.PerformanceBucket, row.Recommendation = productPerformanceOrderMarketDetailRecommendation(*row)
|
||||||
|
filtered = append(filtered, *row)
|
||||||
}
|
}
|
||||||
return out, nil
|
return filtered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceMarketRow, error) {
|
func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceMarketRow, error) {
|
||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 || limit > 500 {
|
if limit <= 0 {
|
||||||
limit = 100
|
limit = 50000
|
||||||
|
} else if limit > 50000 {
|
||||||
|
limit = 50000
|
||||||
}
|
}
|
||||||
rows, err := pg.QueryContext(ctx, `
|
rows, err := pg.QueryContext(ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -2277,6 +2437,7 @@ SELECT
|
|||||||
COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
|
COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY market_key
|
GROUP BY market_key
|
||||||
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
|
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
@@ -2306,8 +2467,10 @@ func ListProductPerformanceCountries(ctx context.Context, pg *sql.DB, limit int)
|
|||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 || limit > 500 {
|
if limit <= 0 {
|
||||||
limit = 100
|
limit = 50000
|
||||||
|
} else if limit > 50000 {
|
||||||
|
limit = 50000
|
||||||
}
|
}
|
||||||
rows, err := pg.QueryContext(ctx, `
|
rows, err := pg.QueryContext(ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -2330,6 +2493,7 @@ SELECT
|
|||||||
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date >= current_date - INTERVAL '364 days'
|
WHERE sales_date >= current_date - INTERVAL '364 days'
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY customer_country, customer_segment, market_key
|
GROUP BY customer_country, customer_segment, market_key
|
||||||
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
@@ -2357,8 +2521,10 @@ func ListProductPerformanceCustomers(ctx context.Context, pg *sql.DB, breakdown
|
|||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 || limit > 500 {
|
if limit <= 0 {
|
||||||
limit = 100
|
limit = 50000
|
||||||
|
} else if limit > 50000 {
|
||||||
|
limit = 50000
|
||||||
}
|
}
|
||||||
mode := strings.ToLower(strings.TrimSpace(breakdown))
|
mode := strings.ToLower(strings.TrimSpace(breakdown))
|
||||||
if mode == "" {
|
if mode == "" {
|
||||||
@@ -2407,6 +2573,7 @@ SELECT
|
|||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date >= current_date - INTERVAL '364 days'
|
WHERE sales_date >= current_date - INTERVAL '364 days'
|
||||||
AND COALESCE(customer_code,'') <> ''
|
AND COALESCE(customer_code,'') <> ''
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY %s
|
GROUP BY %s
|
||||||
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
@@ -2437,7 +2604,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 500
|
limit = 50000
|
||||||
} else if limit > 50000 {
|
} else if limit > 50000 {
|
||||||
limit = 50000
|
limit = 50000
|
||||||
}
|
}
|
||||||
@@ -2566,6 +2733,7 @@ WITH LatestKPI AS (
|
|||||||
urun_alt_grubu
|
urun_alt_grubu
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
||||||
),
|
),
|
||||||
StockAgg AS (
|
StockAgg AS (
|
||||||
@@ -2726,6 +2894,7 @@ Agg AS (
|
|||||||
LEFT JOIN StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
|
LEFT JOIN StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
|
||||||
) s
|
) s
|
||||||
WHERE sales_date >= DATE '2022-01-01'
|
WHERE sales_date >= DATE '2022-01-01'
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY %s
|
GROUP BY %s
|
||||||
),
|
),
|
||||||
Enriched AS (
|
Enriched AS (
|
||||||
@@ -2913,6 +3082,7 @@ LIMIT $1
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
out := make([]models.ProductPerformanceSalesBreakdownRow, 0, limit)
|
out := make([]models.ProductPerformanceSalesBreakdownRow, 0, limit)
|
||||||
|
colorCodes := make([]string, 0, limit)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r models.ProductPerformanceSalesBreakdownRow
|
var r models.ProductPerformanceSalesBreakdownRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -2935,9 +3105,17 @@ LIMIT $1
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
colorCodes = append(colorCodes, r.ColorCode)
|
||||||
out = append(out, r)
|
out = append(out, r)
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
for i := range out {
|
||||||
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductPerformanceGroupedRequest struct {
|
type ProductPerformanceGroupedRequest struct {
|
||||||
@@ -2945,6 +3123,7 @@ type ProductPerformanceGroupedRequest struct {
|
|||||||
GroupLevels []string
|
GroupLevels []string
|
||||||
ExpandedKeys map[string]bool
|
ExpandedKeys map[string]bool
|
||||||
Limit int
|
Limit int
|
||||||
|
MainGroup string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
||||||
@@ -2976,17 +3155,26 @@ type productPerformanceSQLGroupFilter struct {
|
|||||||
|
|
||||||
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
||||||
mode := strings.TrimSpace(req.Mode)
|
mode := strings.TrimSpace(req.Mode)
|
||||||
if mode != "products" && mode != "idle" {
|
if mode != "products" && mode != "product_detail" && mode != "idle" {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, true, err
|
return nil, true, err
|
||||||
}
|
}
|
||||||
out := make([]map[string]any, 0, 512)
|
out := make([]map[string]any, 0, 512)
|
||||||
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, nil, req.ExpandedKeys, req.Limit)
|
filters := productPerformanceGroupedBaseFilters(req)
|
||||||
|
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.Limit)
|
||||||
return out, true, err
|
return out, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
||||||
|
mainGroup := strings.TrimSpace(req.MainGroup)
|
||||||
|
if strings.TrimSpace(req.Mode) != "product_detail" || mainGroup == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []productPerformanceSQLGroupFilter{{Field: "urun_ana_grubu", Value: mainGroup}}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
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) {
|
if level >= len(levels) {
|
||||||
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
|
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
|
||||||
@@ -3135,9 +3323,24 @@ FROM (
|
|||||||
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_total * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_total,
|
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_total * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_total,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_usd_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS avg_price_usd_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_usd_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS avg_price_usd_90d,
|
||||||
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_usd_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS avg_price_usd_180d,
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_usd_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS avg_price_usd_180d,
|
||||||
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS cost_price_usd,
|
CASE
|
||||||
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS base_price_usd,
|
WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0
|
||||||
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS base_price_try,
|
THEN SUM(CASE WHEN sales_qty_total > 0 THEN cost_price_usd * sales_qty_total WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END)
|
||||||
|
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
|
||||||
|
ELSE 0
|
||||||
|
END AS cost_price_usd,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0
|
||||||
|
THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_usd * sales_qty_total WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END)
|
||||||
|
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
|
||||||
|
ELSE 0
|
||||||
|
END AS base_price_usd,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0
|
||||||
|
THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_try * sales_qty_total WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END)
|
||||||
|
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
|
||||||
|
ELSE 0
|
||||||
|
END AS base_price_try,
|
||||||
COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
|
COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
|
||||||
COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d,
|
COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d,
|
||||||
CASE WHEN SUM(sales_usd_90d) > 0 THEN SUM(gross_profit_usd_90d) / NULLIF(SUM(sales_usd_90d),0) ELSE 0 END AS gross_margin_90d,
|
CASE WHEN SUM(sales_usd_90d) > 0 THEN SUM(gross_profit_usd_90d) / NULLIF(SUM(sales_usd_90d),0) ELSE 0 END AS gross_margin_90d,
|
||||||
@@ -3201,6 +3404,7 @@ func queryProductPerformanceJSONRows(ctx context.Context, pg *sql.DB, query stri
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
out := []map[string]any{}
|
out := []map[string]any{}
|
||||||
|
colorCodes := []string{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var raw []byte
|
var raw []byte
|
||||||
if err := rows.Scan(&raw); err != nil {
|
if err := rows.Scan(&raw); err != nil {
|
||||||
@@ -3210,9 +3414,17 @@ func queryProductPerformanceJSONRows(ctx context.Context, pg *sql.DB, query stri
|
|||||||
if err := json.Unmarshal(raw, &row); err != nil {
|
if err := json.Unmarshal(raw, &row); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
colorCodes = append(colorCodes, stringFromMap(row, "color_code"))
|
||||||
out = append(out, row)
|
out = append(out, row)
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
|
||||||
|
for _, row := range out {
|
||||||
|
row["color_description"] = colorDescriptions[normalizeProductPerformanceCode(stringFromMap(row, "color_code"))]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceSQLSourceCTE(mode string) string {
|
func productPerformanceSQLSourceCTE(mode string) string {
|
||||||
@@ -3312,7 +3524,8 @@ Source AS (
|
|||||||
COALESCE(performance_bucket,'') AS performance_bucket,
|
COALESCE(performance_bucket,'') AS performance_bucket,
|
||||||
COALESCE(stock_qty,0) * COALESCE(cost_price_usd,0) AS idle_cost_usd
|
COALESCE(stock_qty,0) * COALESCE(cost_price_usd,0) AS idle_cost_usd
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + idleWhere + `
|
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 + `
|
||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3745,6 +3958,8 @@ func defaultProductPerformanceGroupLevels(mode string) []string {
|
|||||||
switch mode {
|
switch mode {
|
||||||
case "sales_color_yaka_market_customer":
|
case "sales_color_yaka_market_customer":
|
||||||
return []string{"color_code", "yaka_kodu", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
return []string{"color_code", "yaka_kodu", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
||||||
|
case "product_detail":
|
||||||
|
return []string{"urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu", "market_key"}
|
||||||
case "idle":
|
case "idle":
|
||||||
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu"}
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu"}
|
||||||
case "sales_product_country_segment_market_customer":
|
case "sales_product_country_segment_market_customer":
|
||||||
@@ -3980,6 +4195,7 @@ FROM mk_product_performance_sales_daily
|
|||||||
WHERE product_code=$1
|
WHERE product_code=$1
|
||||||
AND color_code=$2
|
AND color_code=$2
|
||||||
AND yaka_kodu=$3
|
AND yaka_kodu=$3
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
ORDER BY sales_usd DESC, sales_date DESC
|
ORDER BY sales_usd DESC, sales_date DESC
|
||||||
LIMIT $4
|
LIMIT $4
|
||||||
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
|
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
|
||||||
@@ -4108,7 +4324,7 @@ ORDER BY SizeCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
|
func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
|
||||||
parts := make([]string, 0, 6)
|
parts := []string{" AND " + productPerformanceAllowedFirstGroupSQL("urun_ilk_grubu")}
|
||||||
args := make([]any, 0, 6)
|
args := make([]any, 0, 6)
|
||||||
add := func(cond string, value any) {
|
add := func(cond string, value any) {
|
||||||
args = append(args, value)
|
args = append(args, value)
|
||||||
@@ -4219,6 +4435,28 @@ func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) strin
|
|||||||
return normalizeProductPerformanceProductCode(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(yakaKodu)
|
return normalizeProductPerformanceProductCode(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(yakaKodu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
|
||||||
|
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD < basePriceUSD {
|
||||||
|
return basePriceUSD, costPriceUSD
|
||||||
|
}
|
||||||
|
return costPriceUSD, basePriceUSD
|
||||||
|
}
|
||||||
|
|
||||||
|
const productPerformanceExcludedFirstGroupsSQL = "('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')"
|
||||||
|
|
||||||
|
func productPerformanceAllowedFirstGroupSQL(expr string) string {
|
||||||
|
return "upper(btrim(COALESCE(" + expr + ",''))) NOT IN " + productPerformanceExcludedFirstGroupsSQL
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExcludedProductPerformanceFirstGroup(value string) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "MALZEMELI FASON", "MALZEMESIZ FASON", "DIGER":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformancePriceLookup(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]productPerformancePriceInfo, error) {
|
func productPerformancePriceLookup(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]productPerformancePriceInfo, error) {
|
||||||
out := make(map[string]productPerformancePriceInfo, len(productCodes))
|
out := make(map[string]productPerformancePriceInfo, len(productCodes))
|
||||||
if len(productCodes) == 0 {
|
if len(productCodes) == 0 {
|
||||||
@@ -4252,6 +4490,7 @@ WHERE product_code = ANY($1)
|
|||||||
if err := rows.Scan(&productCode, &p.cost, &p.base); err != nil {
|
if err := rows.Scan(&productCode, &p.cost, &p.base); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
p.cost, p.base = normalizeProductPerformanceCostPair(p.cost, p.base)
|
||||||
out[normalizeProductPerformanceProductCode(productCode)] = p
|
out[normalizeProductPerformanceProductCode(productCode)] = p
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
@@ -4280,6 +4519,7 @@ SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
|
|||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||||||
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
|
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
||||||
`, pq.Array(variantKeys))
|
`, pq.Array(variantKeys))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ SalesAgg AS (
|
|||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date >= DATE '2022-01-01'
|
WHERE sales_date >= DATE '2022-01-01'
|
||||||
AND sales_date <= $1::date
|
AND sales_date <= $1::date
|
||||||
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY product_code, color_code, yaka_kodu, market_key
|
GROUP BY product_code, color_code, yaka_kodu, market_key
|
||||||
),
|
),
|
||||||
Scope AS (
|
Scope AS (
|
||||||
@@ -476,7 +477,7 @@ Scope AS (
|
|||||||
'' AS seri,
|
'' AS seri,
|
||||||
'' AS yas_grubu,
|
'' AS yas_grubu,
|
||||||
'' AS askili_yan,
|
'' AS askili_yan,
|
||||||
'' AS urun_ilk_grubu,
|
COALESCE(pd.urun_ilk_grubu, '') AS urun_ilk_grubu,
|
||||||
'' AS urun_ana_grubu,
|
'' AS urun_ana_grubu,
|
||||||
'' AS urun_alt_grubu,
|
'' AS urun_alt_grubu,
|
||||||
0 AS sales_qty_30d,
|
0 AS sales_qty_30d,
|
||||||
@@ -494,7 +495,9 @@ Scope AS (
|
|||||||
NULL::date AS last_sale_date,
|
NULL::date AS last_sale_date,
|
||||||
'' AS last_ref_number
|
'' AS last_ref_number
|
||||||
FROM LatestStock ls
|
FROM LatestStock ls
|
||||||
|
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=ls.product_code
|
||||||
WHERE COALESCE(ls.stock_qty, 0) <> 0
|
WHERE COALESCE(ls.stock_qty, 0) <> 0
|
||||||
|
AND upper(btrim(COALESCE(pd.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM SalesAgg sa
|
FROM SalesAgg sa
|
||||||
@@ -503,6 +506,23 @@ Scope AS (
|
|||||||
AND sa.yaka_kodu = ls.yaka_kodu
|
AND sa.yaka_kodu = ls.yaka_kodu
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
PriceDim AS (
|
||||||
|
SELECT
|
||||||
|
product_code,
|
||||||
|
COALESCE(urun_ilk_grubu, '') AS urun_ilk_grubu,
|
||||||
|
COALESCE(urun_ana_grubu, '') AS urun_ana_grubu,
|
||||||
|
COALESCE(urun_alt_grubu, '') AS urun_alt_grubu,
|
||||||
|
CASE
|
||||||
|
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN base_price_usd
|
||||||
|
ELSE cost_price_usd
|
||||||
|
END AS cost_price_usd,
|
||||||
|
CASE
|
||||||
|
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN cost_price_usd
|
||||||
|
ELSE base_price_usd
|
||||||
|
END AS base_price_usd,
|
||||||
|
base_price_try
|
||||||
|
FROM mk_product_performance_price_dim
|
||||||
|
),
|
||||||
Base AS (
|
Base AS (
|
||||||
SELECT
|
SELECT
|
||||||
s.*,
|
s.*,
|
||||||
@@ -538,7 +558,8 @@ Base AS (
|
|||||||
LEFT JOIN Stock180Start s180 ON s180.product_code=s.product_code AND s180.color_code=s.color_code AND s180.yaka_kodu=s.yaka_kodu
|
LEFT JOIN Stock180Start s180 ON s180.product_code=s.product_code AND s180.color_code=s.color_code AND s180.yaka_kodu=s.yaka_kodu
|
||||||
LEFT JOIN Stock365Start s365 ON s365.product_code=s.product_code AND s365.color_code=s.color_code AND s365.yaka_kodu=s.yaka_kodu
|
LEFT JOIN Stock365Start s365 ON s365.product_code=s.product_code AND s365.color_code=s.color_code AND s365.yaka_kodu=s.yaka_kodu
|
||||||
LEFT JOIN StockTotalStart stotal ON stotal.product_code=s.product_code AND stotal.color_code=s.color_code AND stotal.yaka_kodu=s.yaka_kodu
|
LEFT JOIN StockTotalStart stotal ON stotal.product_code=s.product_code AND stotal.color_code=s.color_code AND stotal.yaka_kodu=s.yaka_kodu
|
||||||
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=s.product_code
|
LEFT JOIN PriceDim pd ON pd.product_code=s.product_code
|
||||||
|
WHERE upper(btrim(COALESCE(NULLIF(s.urun_ilk_grubu,''), pd.urun_ilk_grubu, ''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
||||||
),
|
),
|
||||||
Spread AS (
|
Spread AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")),
|
Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")),
|
||||||
SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")),
|
SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")),
|
||||||
Descending: boolQuery(r, "descending", true),
|
Descending: boolQuery(r, "descending", true),
|
||||||
Limit: intQuery(r, "limit", 100),
|
Limit: intQuery(r, "limit", 50000),
|
||||||
Page: intQuery(r, "page", 1),
|
Page: intQuery(r, "page", 1),
|
||||||
}
|
}
|
||||||
rows, total, err := queries.ListProductPerformance(ctx, pg, f)
|
rows, total, err := queries.ListProductPerformance(ctx, pg, f)
|
||||||
@@ -109,7 +109,7 @@ func GetProductPerformanceGeneralHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 500))
|
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 50000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -125,7 +125,7 @@ func GetProductPerformanceOrdersHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 500))
|
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 50000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -145,7 +145,7 @@ func GetProductPerformanceOrderGroupsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx,
|
ctx,
|
||||||
pg,
|
pg,
|
||||||
strings.TrimSpace(r.URL.Query().Get("breakdown")),
|
strings.TrimSpace(r.URL.Query().Get("breakdown")),
|
||||||
intQuery(r, "limit", 500),
|
intQuery(r, "limit", 50000),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "siparis grup analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "siparis grup analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
@@ -162,7 +162,7 @@ func GetProductPerformanceOrderProductCustomersHandler(pg *sql.DB) http.HandlerF
|
|||||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 500))
|
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -178,7 +178,7 @@ func GetProductPerformanceOrderMarketDetailsHandler(pg *sql.DB) http.HandlerFunc
|
|||||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 800))
|
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 50000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -194,7 +194,7 @@ func GetProductPerformanceMarketsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 100))
|
rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 50000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -210,7 +210,7 @@ func GetProductPerformanceCountriesHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 100))
|
rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 50000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -230,7 +230,7 @@ func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx,
|
ctx,
|
||||||
pg,
|
pg,
|
||||||
strings.TrimSpace(r.URL.Query().Get("breakdown")),
|
strings.TrimSpace(r.URL.Query().Get("breakdown")),
|
||||||
intQuery(r, "limit", 100),
|
intQuery(r, "limit", 50000),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "musteri performans kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "musteri performans kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
@@ -251,7 +251,7 @@ func GetProductPerformanceSalesBreakdownHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
ctx,
|
ctx,
|
||||||
pg,
|
pg,
|
||||||
strings.TrimSpace(r.URL.Query().Get("mode")),
|
strings.TrimSpace(r.URL.Query().Get("mode")),
|
||||||
intQuery(r, "limit", 500),
|
intQuery(r, "limit", 50000),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "satis kpi kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "satis kpi kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
@@ -269,6 +269,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
|
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
|
||||||
|
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
|
||||||
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
||||||
limit := intQuery(r, "limit", 50000)
|
limit := intQuery(r, "limit", 50000)
|
||||||
expanded := map[string]bool{}
|
expanded := map[string]bool{}
|
||||||
@@ -278,6 +279,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
GroupLevels []string `json:"group_levels"`
|
GroupLevels []string `json:"group_levels"`
|
||||||
ExpandedKeys []string `json:"expanded_keys"`
|
ExpandedKeys []string `json:"expanded_keys"`
|
||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
|
MainGroup string `json:"urun_ana_grubu"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
|
||||||
@@ -286,6 +288,9 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
if strings.TrimSpace(body.Mode) != "" {
|
if strings.TrimSpace(body.Mode) != "" {
|
||||||
mode = strings.TrimSpace(body.Mode)
|
mode = strings.TrimSpace(body.Mode)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(body.MainGroup) != "" {
|
||||||
|
mainGroup = strings.TrimSpace(body.MainGroup)
|
||||||
|
}
|
||||||
if len(body.GroupLevels) > 0 {
|
if len(body.GroupLevels) > 0 {
|
||||||
groupLevels = body.GroupLevels
|
groupLevels = body.GroupLevels
|
||||||
}
|
}
|
||||||
@@ -312,6 +317,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
GroupLevels: groupLevels,
|
GroupLevels: groupLevels,
|
||||||
ExpandedKeys: expanded,
|
ExpandedKeys: expanded,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
|
MainGroup: mainGroup,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -86,6 +86,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'product_detail'" class="main-group-bar q-mb-xs">
|
||||||
|
<button
|
||||||
|
v-for="option in detailMainGroupOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
:class="['main-group-button', { active: selectedDetailMainGroup === option.value }]"
|
||||||
|
:disabled="pageBusy"
|
||||||
|
@click="selectedDetailMainGroup = option.value"
|
||||||
|
>
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
<small>{{ formatNumber(option.stock_qty, 0) }} stok</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
v-if="activeTab === 'general'"
|
v-if="activeTab === 'general'"
|
||||||
flat
|
flat
|
||||||
@@ -95,7 +109,7 @@
|
|||||||
:rows="filteredGeneralRows"
|
:rows="filteredGeneralRows"
|
||||||
:columns="generalColumns"
|
:columns="generalColumns"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score_total', descending: true }"
|
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
>
|
>
|
||||||
@@ -206,7 +220,7 @@
|
|||||||
:rows="displayOrderProductCustomerTableRows"
|
:rows="displayOrderProductCustomerTableRows"
|
||||||
:columns="orderProductCustomerColumns"
|
:columns="orderProductCustomerColumns"
|
||||||
:loading="loading || backendGroupedLoading"
|
:loading="loading || backendGroupedLoading"
|
||||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_usd', descending: true }"
|
:pagination="{ rowsPerPage: 0, sortBy: 'order_usd', descending: true }"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -354,7 +368,7 @@
|
|||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ props.row.label }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -396,7 +410,7 @@
|
|||||||
:rows="displayOrderMarketDetailTableRows"
|
:rows="displayOrderMarketDetailTableRows"
|
||||||
:columns="orderMarketDetailColumns"
|
:columns="orderMarketDetailColumns"
|
||||||
:loading="loading || backendGroupedLoading"
|
:loading="loading || backendGroupedLoading"
|
||||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_date', descending: true }"
|
:pagination="{ rowsPerPage: 0, sortBy: 'order_date', descending: true }"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -508,7 +522,7 @@
|
|||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ props.row.label }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -525,13 +539,13 @@
|
|||||||
</q-table>
|
</q-table>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
v-else-if="activeTab === 'products'"
|
v-else-if="isProductKpiTab"
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
row-key="row_key"
|
row-key="row_key"
|
||||||
class="performance-table product-breakdown-table bg-white"
|
class="performance-table product-breakdown-table bg-white"
|
||||||
:rows="displayProductTableRows"
|
:rows="displayProductKpiTableRows"
|
||||||
:columns="productColumns"
|
:columns="activeProductColumns"
|
||||||
:loading="loading || backendGroupedLoading"
|
:loading="loading || backendGroupedLoading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
@@ -549,17 +563,17 @@
|
|||||||
round
|
round
|
||||||
size="8px"
|
size="8px"
|
||||||
icon="filter_alt"
|
icon="filter_alt"
|
||||||
:color="hasColumnFilter('products', props.col.name) ? 'primary' : 'grey-7'"
|
:color="hasColumnFilter(activeProductTableKey, props.col.name) ? 'primary' : 'grey-7'"
|
||||||
class="header-filter-btn"
|
class="header-filter-btn"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<q-badge v-if="hasColumnFilter('products', props.col.name)" color="primary" floating rounded>
|
<q-badge v-if="hasColumnFilter(activeProductTableKey, props.col.name)" color="primary" floating rounded>
|
||||||
{{ getColumnFilterBadgeValue('products', props.col.name) }}
|
{{ getColumnFilterBadgeValue(activeProductTableKey, props.col.name) }}
|
||||||
</q-badge>
|
</q-badge>
|
||||||
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||||
<div class="excel-filter-menu">
|
<div class="excel-filter-menu">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="columnFilterSearch[filterKey('products', props.col.name)]"
|
v-model="columnFilterSearch[filterKey(activeProductTableKey, props.col.name)]"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
clearable
|
clearable
|
||||||
@@ -567,13 +581,13 @@
|
|||||||
placeholder="Ara"
|
placeholder="Ara"
|
||||||
/>
|
/>
|
||||||
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||||
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions('products', props.col.name)" />
|
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions(activeProductTableKey, props.col.name)" />
|
||||||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('products', props.col.name)" />
|
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(activeProductTableKey, props.col.name)" />
|
||||||
</div>
|
</div>
|
||||||
<q-virtual-scroll
|
<q-virtual-scroll
|
||||||
v-if="columnFilterOptions('products', props.col.name).length > 0"
|
v-if="columnFilterOptions(activeProductTableKey, props.col.name).length > 0"
|
||||||
class="excel-filter-options"
|
class="excel-filter-options"
|
||||||
:items="columnFilterOptions('products', props.col.name)"
|
:items="columnFilterOptions(activeProductTableKey, props.col.name)"
|
||||||
:virtual-scroll-item-size="32"
|
:virtual-scroll-item-size="32"
|
||||||
separator
|
separator
|
||||||
>
|
>
|
||||||
@@ -583,14 +597,14 @@
|
|||||||
dense
|
dense
|
||||||
clickable
|
clickable
|
||||||
class="excel-filter-option"
|
class="excel-filter-option"
|
||||||
@click="toggleColumnFilterValue('products', props.col.name, option.value)"
|
@click="toggleColumnFilterValue(activeProductTableKey, props.col.name, option.value)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="isColumnFilterValueSelected('products', props.col.name, option.value)"
|
:model-value="isColumnFilterValueSelected(activeProductTableKey, props.col.name, option.value)"
|
||||||
@update:model-value="() => toggleColumnFilterValue('products', props.col.name, option.value)"
|
@update:model-value="() => toggleColumnFilterValue(activeProductTableKey, props.col.name, option.value)"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
@@ -646,7 +660,7 @@
|
|||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ props.row.label }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ formatGroupCell(props.row, col.name) }}</span>
|
<span v-else>{{ formatGroupCell(props.row, col.name) }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -688,7 +702,7 @@
|
|||||||
:rows="displayIdleTableRows"
|
:rows="displayIdleTableRows"
|
||||||
:columns="idleColumns"
|
:columns="idleColumns"
|
||||||
:loading="loading || backendGroupedLoading"
|
:loading="loading || backendGroupedLoading"
|
||||||
:pagination="{ rowsPerPage: 100 }"
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -832,7 +846,7 @@
|
|||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ props.row.label }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -874,7 +888,7 @@
|
|||||||
:rows="filteredMarketRows"
|
:rows="filteredMarketRows"
|
||||||
:columns="marketColumns"
|
:columns="marketColumns"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="{ rowsPerPage: 100 }"
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
>
|
>
|
||||||
@@ -974,7 +988,7 @@
|
|||||||
:rows="displayActiveSalesBreakdownTableRows"
|
:rows="displayActiveSalesBreakdownTableRows"
|
||||||
:columns="visibleSalesBreakdownColumns"
|
:columns="visibleSalesBreakdownColumns"
|
||||||
:loading="loading || backendGroupedLoading"
|
:loading="loading || backendGroupedLoading"
|
||||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score_total', descending: true }"
|
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -1091,7 +1105,7 @@
|
|||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ props.row.label }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -1132,7 +1146,7 @@
|
|||||||
:rows="filteredCustomerRows"
|
:rows="filteredCustomerRows"
|
||||||
:columns="customerColumns"
|
:columns="customerColumns"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="{ rowsPerPage: 100 }"
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
>
|
>
|
||||||
@@ -1233,7 +1247,7 @@
|
|||||||
:rows="filteredCountryRows"
|
:rows="filteredCountryRows"
|
||||||
:columns="countryColumns"
|
:columns="countryColumns"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="{ rowsPerPage: 100 }"
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
>
|
>
|
||||||
@@ -1476,10 +1490,26 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|||||||
import { Notify } from 'quasar'
|
import { Notify } from 'quasar'
|
||||||
import api from 'src/services/api'
|
import api from 'src/services/api'
|
||||||
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
||||||
import { formatMoney, formatNumber, formatPercent } from 'src/utils/formatters'
|
|
||||||
|
|
||||||
const performanceStore = useProductPerformanceStore()
|
const performanceStore = useProductPerformanceStore()
|
||||||
|
|
||||||
|
function formatNumber (value, fraction = 2) {
|
||||||
|
const n = Number(value || 0)
|
||||||
|
const digits = Math.max(0, Math.min(2, Number(fraction || 0)))
|
||||||
|
return n.toLocaleString('tr-TR', {
|
||||||
|
minimumFractionDigits: digits,
|
||||||
|
maximumFractionDigits: digits
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMoney (value, currency = 'USD') {
|
||||||
|
return `${formatNumber(value, 2)} ${currency}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent (value) {
|
||||||
|
return `${formatNumber(Number(value || 0) * 100, 2)}%`
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const generalRows = ref([])
|
const generalRows = ref([])
|
||||||
@@ -1505,6 +1535,7 @@ const summary = ref({})
|
|||||||
const activeTab = ref('products')
|
const activeTab = ref('products')
|
||||||
const customerBreakdown = ref('market_customer')
|
const customerBreakdown = ref('market_customer')
|
||||||
const selectedPeriods = ref(['90', '180', '360', 'total'])
|
const selectedPeriods = ref(['90', '180', '360', 'total'])
|
||||||
|
const selectedDetailMainGroup = ref('')
|
||||||
const imageUrlByKey = ref({})
|
const imageUrlByKey = ref({})
|
||||||
const imageListByKey = ref({})
|
const imageListByKey = ref({})
|
||||||
const imageDialog = ref(false)
|
const imageDialog = ref(false)
|
||||||
@@ -1533,6 +1564,7 @@ const expandedGroups = ref({})
|
|||||||
const lastManualExpandedGroupKey = ref('')
|
const lastManualExpandedGroupKey = ref('')
|
||||||
const selectedExpandLevelKeysByTab = reactive({
|
const selectedExpandLevelKeysByTab = reactive({
|
||||||
products: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
products: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
||||||
|
product_detail: ['urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'color_code', 'yaka_kodu'],
|
||||||
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'urun_ilk_grubu', 'askili_yan'],
|
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'urun_ilk_grubu', 'askili_yan'],
|
||||||
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
||||||
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||||||
@@ -1542,10 +1574,12 @@ const selectedExpandLevelKeysByTab = reactive({
|
|||||||
order_market_details: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan']
|
order_market_details: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan']
|
||||||
})
|
})
|
||||||
const productKpiFetchLimit = 50000
|
const productKpiFetchLimit = 50000
|
||||||
|
const reportFetchLimit = 50000
|
||||||
const maxBulkExpandKeys = 1200
|
const maxBulkExpandKeys = 1200
|
||||||
const performanceTabs = [
|
const performanceTabs = [
|
||||||
{ name: 'products', icon: 'inventory_2', label: 'Ürün KPI' },
|
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
||||||
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk > Yaka > Piyasa > Müşteri' },
|
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
|
||||||
|
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
|
||||||
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
||||||
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
||||||
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
|
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
|
||||||
@@ -1556,7 +1590,7 @@ const pageBusy = computed(() => loading.value || backendGroupedLoading.value ||
|
|||||||
|
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
page: 1,
|
page: 1,
|
||||||
rowsPerPage: 100,
|
rowsPerPage: 0,
|
||||||
sortBy: 'performance_score_total',
|
sortBy: 'performance_score_total',
|
||||||
descending: true
|
descending: true
|
||||||
})
|
})
|
||||||
@@ -1691,6 +1725,7 @@ const periodOrder = {
|
|||||||
const hiddenTableColumns = new Set(['item_description', 'performance_score'])
|
const hiddenTableColumns = new Set(['item_description', 'performance_score'])
|
||||||
const dimensionColumnNames = [
|
const dimensionColumnNames = [
|
||||||
'product_code',
|
'product_code',
|
||||||
|
'color_yaka',
|
||||||
'color_code',
|
'color_code',
|
||||||
'yaka_kodu',
|
'yaka_kodu',
|
||||||
'item_description',
|
'item_description',
|
||||||
@@ -1824,6 +1859,10 @@ function reorderColumnsInPlace (targetColumns) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productColumns = computed(() => orderedMetricColumns(columns.filter(visiblePeriodColumn)))
|
const productColumns = computed(() => orderedMetricColumns(columns.filter(visiblePeriodColumn)))
|
||||||
|
const productDetailColumns = computed(() => orderedMetricColumns(columns
|
||||||
|
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
||||||
|
.filter(visiblePeriodColumn)))
|
||||||
|
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? productDetailColumns.value : productColumns.value)
|
||||||
|
|
||||||
const generalColumns = [
|
const generalColumns = [
|
||||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||||
@@ -2158,6 +2197,36 @@ function removeColumnsInPlace (targetColumns, names) {
|
|||||||
return targetColumns
|
return targetColumns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureColorYakaColumn (targetColumns) {
|
||||||
|
const colorIndex = targetColumns.findIndex(col => col.name === 'color_code')
|
||||||
|
const yakaIndex = targetColumns.findIndex(col => col.name === 'yaka_kodu')
|
||||||
|
const insertAt = colorIndex >= 0 ? colorIndex : yakaIndex
|
||||||
|
if (insertAt >= 0 && !targetColumns.some(col => col.name === 'color_yaka')) {
|
||||||
|
targetColumns.splice(insertAt, 0, {
|
||||||
|
name: 'color_yaka',
|
||||||
|
label: 'Renk/Yaka',
|
||||||
|
field: row => colorYakaValue(row),
|
||||||
|
align: 'left',
|
||||||
|
sortable: true,
|
||||||
|
classes: 'color-yaka-cell',
|
||||||
|
headerClasses: 'color-yaka-cell'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
removeColumnsInPlace(targetColumns, ['color_code', 'yaka_kodu'])
|
||||||
|
return targetColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
;[
|
||||||
|
columns,
|
||||||
|
generalColumns,
|
||||||
|
orderAnalysisColumns,
|
||||||
|
orderGroupColumns,
|
||||||
|
orderProductCustomerColumns,
|
||||||
|
orderMarketDetailColumns,
|
||||||
|
idleColumns,
|
||||||
|
salesBreakdownColumns
|
||||||
|
].forEach(ensureColorYakaColumn)
|
||||||
|
|
||||||
;[
|
;[
|
||||||
columns,
|
columns,
|
||||||
generalColumns,
|
generalColumns,
|
||||||
@@ -2338,6 +2407,14 @@ const groupLevels = [
|
|||||||
|
|
||||||
const tabGroupLevels = {
|
const tabGroupLevels = {
|
||||||
products: groupLevels,
|
products: groupLevels,
|
||||||
|
product_detail: [
|
||||||
|
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' },
|
||||||
|
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||||
|
{ key: 'product_code', label: 'Ürün' },
|
||||||
|
{ key: 'color_code', label: 'Renk' },
|
||||||
|
{ key: 'yaka_kodu', label: 'Yaka' },
|
||||||
|
{ key: 'market_key', label: 'Piyasa' }
|
||||||
|
],
|
||||||
sales_color_yaka_market_customer: [
|
sales_color_yaka_market_customer: [
|
||||||
{ key: 'color_code', label: 'Renk' },
|
{ key: 'color_code', label: 'Renk' },
|
||||||
{ key: 'yaka_kodu', label: 'Yaka' },
|
{ key: 'yaka_kodu', label: 'Yaka' },
|
||||||
@@ -2459,6 +2536,7 @@ const autoExpandThroughLevel = computed(() => {
|
|||||||
|
|
||||||
const tableFilterSourceMap = computed(() => ({
|
const tableFilterSourceMap = computed(() => ({
|
||||||
products: { rows: rows.value, columns: productColumns.value },
|
products: { rows: rows.value, columns: productColumns.value },
|
||||||
|
product_detail: { rows: detailProductRows.value, columns: productDetailColumns.value },
|
||||||
general: { rows: generalRows.value, columns: generalColumns },
|
general: { rows: generalRows.value, columns: generalColumns },
|
||||||
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
|
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
|
||||||
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
|
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
|
||||||
@@ -2473,6 +2551,14 @@ const tableFilterSourceMap = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value))
|
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value))
|
||||||
|
const isProductKpiTab = computed(() => activeTab.value === 'products' || activeTab.value === 'product_detail')
|
||||||
|
const activeProductTableKey = computed(() => activeTab.value === 'product_detail' ? 'product_detail' : 'products')
|
||||||
|
const detailProductRows = computed(() => {
|
||||||
|
const selected = String(selectedDetailMainGroup.value || '').trim()
|
||||||
|
if (!selected) return rows.value
|
||||||
|
return rows.value.filter(row => productMainGroupValue(row) === selected)
|
||||||
|
})
|
||||||
|
const filteredDetailProductRows = computed(() => filterRowsForTable('product_detail', detailProductRows.value, productDetailColumns.value))
|
||||||
const filteredGeneralRows = computed(() => filterRowsForTable('general', generalRows.value, generalColumns))
|
const filteredGeneralRows = computed(() => filterRowsForTable('general', generalRows.value, generalColumns))
|
||||||
const filteredOrderProductCustomerRows = computed(() => filterRowsForTable('order_product_customers', orderProductCustomerRows.value, orderProductCustomerColumns))
|
const filteredOrderProductCustomerRows = computed(() => filterRowsForTable('order_product_customers', orderProductCustomerRows.value, orderProductCustomerColumns))
|
||||||
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable('order_market_details', orderMarketDetailRows.value, orderMarketDetailColumns))
|
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable('order_market_details', orderMarketDetailRows.value, orderMarketDetailColumns))
|
||||||
@@ -2483,6 +2569,7 @@ const filteredCustomerRows = computed(() => filterRowsForTable('customers', cust
|
|||||||
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
|
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
|
||||||
const activeGroupSourceRows = computed(() => {
|
const activeGroupSourceRows = computed(() => {
|
||||||
if (activeTab.value === 'products') return filteredProductRows.value
|
if (activeTab.value === 'products') return filteredProductRows.value
|
||||||
|
if (activeTab.value === 'product_detail') return filteredDetailProductRows.value
|
||||||
if (activeTab.value === 'idle') return filteredIdleRows.value
|
if (activeTab.value === 'idle') return filteredIdleRows.value
|
||||||
if (activeTab.value === 'order_product_customers') return filteredOrderProductCustomerRows.value
|
if (activeTab.value === 'order_product_customers') return filteredOrderProductCustomerRows.value
|
||||||
if (activeTab.value === 'order_market_details') return filteredOrderMarketDetailRows.value
|
if (activeTab.value === 'order_market_details') return filteredOrderMarketDetailRows.value
|
||||||
@@ -2519,17 +2606,34 @@ const productTableRows = computed(() => {
|
|||||||
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
||||||
return out
|
return out
|
||||||
})
|
})
|
||||||
|
const detailProductTableRows = computed(() => buildGroupedTableRows('product_detail', filteredDetailProductRows.value))
|
||||||
|
|
||||||
const idleTableRows = computed(() => buildGroupedTableRows('idle', filteredIdleRows.value))
|
const idleTableRows = computed(() => buildGroupedTableRows('idle', filteredIdleRows.value))
|
||||||
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
||||||
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
||||||
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
||||||
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
||||||
|
const displayDetailProductTableRows = computed(() => backendRowsForTab('product_detail', detailProductTableRows.value))
|
||||||
|
const displayProductKpiTableRows = computed(() => activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : displayProductTableRows.value)
|
||||||
const displayIdleTableRows = computed(() => backendRowsForTab('idle', idleTableRows.value))
|
const displayIdleTableRows = computed(() => backendRowsForTab('idle', idleTableRows.value))
|
||||||
const displayOrderProductCustomerTableRows = computed(() => backendRowsForTab('order_product_customers', orderProductCustomerTableRows.value))
|
const displayOrderProductCustomerTableRows = computed(() => backendRowsForTab('order_product_customers', orderProductCustomerTableRows.value))
|
||||||
const displayOrderMarketDetailTableRows = computed(() => backendRowsForTab('order_market_details', orderMarketDetailTableRows.value))
|
const displayOrderMarketDetailTableRows = computed(() => backendRowsForTab('order_market_details', orderMarketDetailTableRows.value))
|
||||||
const displayActiveSalesBreakdownTableRows = computed(() => backendRowsForTab(activeTab.value, activeSalesBreakdownTableRows.value))
|
const displayActiveSalesBreakdownTableRows = computed(() => backendRowsForTab(activeTab.value, activeSalesBreakdownTableRows.value))
|
||||||
|
|
||||||
|
const detailMainGroupOptions = computed(() => {
|
||||||
|
const grouped = new Map()
|
||||||
|
for (const row of rows.value) {
|
||||||
|
const value = productMainGroupValue(row)
|
||||||
|
if (!value) continue
|
||||||
|
const current = grouped.get(value) || { value, label: value, stock_qty: 0, sales_qty_90d: 0 }
|
||||||
|
current.stock_qty += Number(row?.stock_qty || 0)
|
||||||
|
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
|
||||||
|
grouped.set(value, current)
|
||||||
|
}
|
||||||
|
return Array.from(grouped.values())
|
||||||
|
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, 'tr'))
|
||||||
|
})
|
||||||
|
|
||||||
const productGroupKeys = computed(() => {
|
const productGroupKeys = computed(() => {
|
||||||
const keys = []
|
const keys = []
|
||||||
collectGroupKeys(keys, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
collectGroupKeys(keys, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
||||||
@@ -2725,6 +2829,9 @@ function sortValueForColumnName (row, columnName) {
|
|||||||
function makeGroupRow (key, level, groupDef, value, groupRows) {
|
function makeGroupRow (key, level, groupDef, value, groupRows) {
|
||||||
const imageSource = groupRows.find(row => row?.product_code) || {}
|
const imageSource = groupRows.find(row => row?.product_code) || {}
|
||||||
const aggregate = aggregateGroupFields(groupRows, groupDef.key)
|
const aggregate = aggregateGroupFields(groupRows, groupDef.key)
|
||||||
|
const colorSummary = groupDef.key === 'color_code' ? value : distinctSummary(groupRows, 'color_code')
|
||||||
|
const yakaSummary = groupDef.key === 'yaka_kodu' ? value : distinctSummary(groupRows, 'yaka_kodu')
|
||||||
|
const colorDescriptionSummary = distinctSummary(groupRows, 'color_description')
|
||||||
return {
|
return {
|
||||||
__group: true,
|
__group: true,
|
||||||
row_key: `group|${key}`,
|
row_key: `group|${key}`,
|
||||||
@@ -2732,14 +2839,16 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
|
|||||||
level,
|
level,
|
||||||
group_field: groupDef.key,
|
group_field: groupDef.key,
|
||||||
group_value: value,
|
group_value: value,
|
||||||
label: value,
|
label: colorYakaGroupLabel(groupDef.key, value, colorSummary, yakaSummary),
|
||||||
count: groupRows.length,
|
count: groupRows.length,
|
||||||
image_product_code: imageSource.product_code || '',
|
image_product_code: imageSource.product_code || '',
|
||||||
image_color_code: imageSource.color_code || '',
|
image_color_code: imageSource.color_code || '',
|
||||||
|
image_color_description: imageSource.color_description || '',
|
||||||
image_yaka_kodu: imageSource.yaka_kodu || '',
|
image_yaka_kodu: imageSource.yaka_kodu || '',
|
||||||
product_code: groupDef.key === 'product_code' ? value : distinctSummary(groupRows, 'product_code'),
|
product_code: groupDef.key === 'product_code' ? value : distinctSummary(groupRows, 'product_code'),
|
||||||
color_code: groupDef.key === 'color_code' ? value : distinctSummary(groupRows, 'color_code'),
|
color_code: colorSummary,
|
||||||
yaka_kodu: groupDef.key === 'yaka_kodu' ? value : distinctSummary(groupRows, 'yaka_kodu'),
|
color_description: colorDescriptionSummary,
|
||||||
|
yaka_kodu: yakaSummary,
|
||||||
item_description: distinctSummary(groupRows, 'item_description'),
|
item_description: distinctSummary(groupRows, 'item_description'),
|
||||||
kategori: groupDef.key === 'kategori' ? value : distinctSummary(groupRows, 'kategori'),
|
kategori: groupDef.key === 'kategori' ? value : distinctSummary(groupRows, 'kategori'),
|
||||||
urun_ilk_grubu: groupDef.key === 'urun_ilk_grubu' ? value : distinctSummary(groupRows, 'urun_ilk_grubu'),
|
urun_ilk_grubu: groupDef.key === 'urun_ilk_grubu' ? value : distinctSummary(groupRows, 'urun_ilk_grubu'),
|
||||||
@@ -3170,7 +3279,17 @@ function expandSelectedProductGroups () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupLabelColumnName (row) {
|
function groupLabelColumnName (row) {
|
||||||
return row?.group_field || 'product_code'
|
const field = String(row?.group_field || '')
|
||||||
|
if (field === 'color_code' || field === 'yaka_kodu') return 'color_yaka'
|
||||||
|
return field || 'product_code'
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupDisplayLabel (row) {
|
||||||
|
const field = String(row?.group_field || '')
|
||||||
|
if (field === 'color_code' || field === 'yaka_kodu') {
|
||||||
|
return colorYakaValue(row) || row?.label || row?.group_value || ''
|
||||||
|
}
|
||||||
|
return row?.label || row?.group_value || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupIndentPx (row) {
|
function groupIndentPx (row) {
|
||||||
@@ -3209,6 +3328,7 @@ function formatGroupCell (row, colOrName) {
|
|||||||
const name = colOrName?.name || colOrName
|
const name = colOrName?.name || colOrName
|
||||||
if (name === 'image') return ''
|
if (name === 'image') return ''
|
||||||
if (name === 'recommendation') return row.recommendation || ''
|
if (name === 'recommendation') return row.recommendation || ''
|
||||||
|
if (name === 'color_yaka' && ['color_code', 'yaka_kodu'].includes(String(row?.group_field || ''))) return colorYakaValue(row)
|
||||||
if (groupDimensionFields.has(name) && name !== row?.group_field) return ''
|
if (groupDimensionFields.has(name) && name !== row?.group_field) return ''
|
||||||
return formatTableCell(row, colOrName)
|
return formatTableCell(row, colOrName)
|
||||||
}
|
}
|
||||||
@@ -3347,6 +3467,42 @@ function columnFilterLabel (row, col) {
|
|||||||
return formattedColumnCellValue(row, col) || columnFilterValue(row, col)
|
return formattedColumnCellValue(row, col) || columnFilterValue(row, col)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function colorYakaValue (row) {
|
||||||
|
const groupField = String(row?.group_field || '')
|
||||||
|
const groupValue = String(row?.group_value || row?.label || '').trim()
|
||||||
|
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||||||
|
const colorDescription = String(row?.image_color_description || row?.color_description || '').trim()
|
||||||
|
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
|
||||||
|
const colorLabel = colorDisplayLabel(groupField === 'color_code' ? groupValue || color : color, colorDescription)
|
||||||
|
if (row?.__group && groupField === 'color_code') return colorLabel
|
||||||
|
if (row?.__group && groupField === 'yaka_kodu') {
|
||||||
|
const parentColor = colorLabel && colorLabel !== '-' ? colorLabel : ''
|
||||||
|
const groupYaka = groupValue && groupValue !== '-' ? groupValue : yaka
|
||||||
|
return [parentColor, groupYaka].filter(Boolean).join('/')
|
||||||
|
}
|
||||||
|
return [colorLabel, yaka].filter(value => value && value !== '-').join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorDisplayLabel (code, description) {
|
||||||
|
const colorCode = String(code || '').trim()
|
||||||
|
const desc = String(description || '').trim()
|
||||||
|
if (!colorCode) return ''
|
||||||
|
if (!desc) return colorCode
|
||||||
|
return `${colorCode}-${desc.toLocaleUpperCase('tr-TR')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorYakaGroupLabel (groupField, groupValue, colorSummary, yakaSummary) {
|
||||||
|
const field = String(groupField || '')
|
||||||
|
const value = String(groupValue || '').trim()
|
||||||
|
if (field === 'color_code') return value
|
||||||
|
if (field === 'yaka_kodu') {
|
||||||
|
const color = String(colorSummary || '').trim()
|
||||||
|
const yaka = value || String(yakaSummary || '').trim()
|
||||||
|
return [color, yaka].filter(part => part && part !== '-').join('/')
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
function productCategoryValue (row) {
|
function productCategoryValue (row) {
|
||||||
return row?.kategori || ''
|
return row?.kategori || ''
|
||||||
}
|
}
|
||||||
@@ -3380,6 +3536,7 @@ function cleanProductFirstGroupValue (value) {
|
|||||||
|
|
||||||
function rawProductCellValue (row, name) {
|
function rawProductCellValue (row, name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case 'color_yaka': return colorYakaValue(row)
|
||||||
case 'kategori': return productCategoryValue(row)
|
case 'kategori': return productCategoryValue(row)
|
||||||
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
||||||
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
||||||
@@ -3393,6 +3550,7 @@ function rawProductCellValue (row, name) {
|
|||||||
function rawColumnCellValue (row, col) {
|
function rawColumnCellValue (row, col) {
|
||||||
const name = col?.name || col
|
const name = col?.name || col
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case 'color_yaka': return colorYakaValue(row)
|
||||||
case 'kategori': return productCategoryValue(row)
|
case 'kategori': return productCategoryValue(row)
|
||||||
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
||||||
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
||||||
@@ -3469,6 +3627,7 @@ function formatTableCell (row, colOrName) {
|
|||||||
const col = typeof colOrName === 'object' ? colOrName : null
|
const col = typeof colOrName === 'object' ? colOrName : null
|
||||||
const name = col?.name || colOrName
|
const name = col?.name || colOrName
|
||||||
if (name === 'image') return ''
|
if (name === 'image') return ''
|
||||||
|
if (name === 'color_yaka') return colorYakaValue(row)
|
||||||
if (['kategori', 'urun_ilk_grubu', 'urun_ana_grubu'].includes(name)) return rawProductCellValue(row, name) || ''
|
if (['kategori', 'urun_ilk_grubu', 'urun_ana_grubu'].includes(name)) return rawProductCellValue(row, name) || ''
|
||||||
if (name === 'market_key') return displayMarketName(row.market_key)
|
if (name === 'market_key') return displayMarketName(row.market_key)
|
||||||
if (name === 'performance_bucket') return bucketLabel(row.performance_bucket)
|
if (name === 'performance_bucket') return bucketLabel(row.performance_bucket)
|
||||||
@@ -3897,6 +4056,7 @@ async function loadProductDetailRows (row) {
|
|||||||
|
|
||||||
function backendGroupedSupportedTab (tabKey = activeTab.value) {
|
function backendGroupedSupportedTab (tabKey = activeTab.value) {
|
||||||
return tabKey === 'products' ||
|
return tabKey === 'products' ||
|
||||||
|
tabKey === 'product_detail' ||
|
||||||
tabKey === 'idle' ||
|
tabKey === 'idle' ||
|
||||||
tabKey === 'order_product_customers' ||
|
tabKey === 'order_product_customers' ||
|
||||||
tabKey === 'order_market_details' ||
|
tabKey === 'order_market_details' ||
|
||||||
@@ -3956,7 +4116,10 @@ async function loadBackendGroupedRows () {
|
|||||||
mode: tabKey,
|
mode: tabKey,
|
||||||
groupLevels: activeGroupLevels.value.map(level => level.key),
|
groupLevels: activeGroupLevels.value.map(level => level.key),
|
||||||
expandedKeys: activeExpandedGroupKeys(),
|
expandedKeys: activeExpandedGroupKeys(),
|
||||||
limit: tabKey === 'products' || tabKey === 'idle' ? productKpiFetchLimit : 50000
|
limit: tabKey === 'products' || tabKey === 'product_detail' || tabKey === 'idle' ? productKpiFetchLimit : 50000
|
||||||
|
}
|
||||||
|
if (tabKey === 'product_detail') {
|
||||||
|
params.urunAnaGrubu = selectedDetailMainGroup.value
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const startedAt = performance.now()
|
const startedAt = performance.now()
|
||||||
@@ -3985,7 +4148,7 @@ function normalizeGroupedDisplayRow (tabKey, row) {
|
|||||||
return withOrderPeriodScores(nextRow)
|
return withOrderPeriodScores(nextRow)
|
||||||
}
|
}
|
||||||
const next = withPeriodScores(nextRow)
|
const next = withPeriodScores(nextRow)
|
||||||
if (tabKey !== 'products' && tabKey !== 'idle') {
|
if (tabKey !== 'products' && tabKey !== 'product_detail' && tabKey !== 'idle') {
|
||||||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||||||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||||||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||||||
@@ -4042,7 +4205,7 @@ async function reload () {
|
|||||||
try {
|
try {
|
||||||
const salesBreakdownRequests = salesBreakdownTabKeys.map(tabKey => {
|
const salesBreakdownRequests = salesBreakdownTabKeys.map(tabKey => {
|
||||||
return timedProductPerformanceGet(`sales-breakdown:${tabKey}`, '/pricing/product-performance/sales-breakdown', {
|
return timedProductPerformanceGet(`sales-breakdown:${tabKey}`, '/pricing/product-performance/sales-breakdown', {
|
||||||
params: { limit: 800, mode: salesBreakdownModes[tabKey] },
|
params: { limit: reportFetchLimit, mode: salesBreakdownModes[tabKey] },
|
||||||
timeout: 60000
|
timeout: 60000
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -4055,11 +4218,11 @@ async function reload () {
|
|||||||
},
|
},
|
||||||
timeout: 60000
|
timeout: 60000
|
||||||
}),
|
}),
|
||||||
timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: 500 }, timeout: 60000 }),
|
timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: reportFetchLimit }, timeout: 60000 }),
|
||||||
timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: 500 }, timeout: 60000 }),
|
timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 60000 }),
|
||||||
timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: 500 }, timeout: 60000 }),
|
timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: reportFetchLimit }, timeout: 60000 }),
|
||||||
timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
|
timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
|
||||||
params: { limit: 500, breakdown: customerBreakdown.value },
|
params: { limit: reportFetchLimit, breakdown: customerBreakdown.value },
|
||||||
timeout: 60000
|
timeout: 60000
|
||||||
}),
|
}),
|
||||||
...salesBreakdownRequests
|
...salesBreakdownRequests
|
||||||
@@ -4101,7 +4264,7 @@ async function reload () {
|
|||||||
async function loadOrderProductCustomers (showLoading = true) {
|
async function loadOrderProductCustomers (showLoading = true) {
|
||||||
if (showLoading) loading.value = true
|
if (showLoading) loading.value = true
|
||||||
try {
|
try {
|
||||||
const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: 500 }, timeout: 90000 })
|
const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||||||
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
|
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
|
||||||
orderProductCustomersLoaded.value = true
|
orderProductCustomersLoaded.value = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -4114,7 +4277,7 @@ async function loadOrderProductCustomers (showLoading = true) {
|
|||||||
async function loadOrderMarketDetails (showLoading = true) {
|
async function loadOrderMarketDetails (showLoading = true) {
|
||||||
if (showLoading) loading.value = true
|
if (showLoading) loading.value = true
|
||||||
try {
|
try {
|
||||||
const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: 800 }, timeout: 90000 })
|
const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||||||
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
|
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
|
||||||
orderMarketDetailsLoaded.value = true
|
orderMarketDetailsLoaded.value = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -4128,9 +4291,9 @@ async function loadOrderAnalysis (showLoading = true) {
|
|||||||
if (showLoading) loading.value = true
|
if (showLoading) loading.value = true
|
||||||
try {
|
try {
|
||||||
const [productResp, marketResp, customerResp] = await Promise.all([
|
const [productResp, marketResp, customerResp] = await Promise.all([
|
||||||
timedProductPerformanceGet('orders:products', '/pricing/product-performance/orders', { params: { limit: 500 }, timeout: 90000 }),
|
timedProductPerformanceGet('orders:products', '/pricing/product-performance/orders', { params: { limit: reportFetchLimit }, timeout: 90000 }),
|
||||||
timedProductPerformanceGet('orders:groups:market', '/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'market' }, timeout: 90000 }),
|
timedProductPerformanceGet('orders:groups:market', '/pricing/product-performance/orders/groups', { params: { limit: reportFetchLimit, breakdown: 'market' }, timeout: 90000 }),
|
||||||
timedProductPerformanceGet('orders:groups:customer', '/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'customer' }, timeout: 90000 })
|
timedProductPerformanceGet('orders:groups:customer', '/pricing/product-performance/orders/groups', { params: { limit: reportFetchLimit, breakdown: 'customer' }, timeout: 90000 })
|
||||||
])
|
])
|
||||||
orderAnalysisRows.value = (Array.isArray(productResp?.data) ? productResp.data : []).map(normalizeOrderAnalysisRow)
|
orderAnalysisRows.value = (Array.isArray(productResp?.data) ? productResp.data : []).map(normalizeOrderAnalysisRow)
|
||||||
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
|
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
|
||||||
@@ -4173,9 +4336,33 @@ watch(activeSelectedExpandLevelKeys, () => {
|
|||||||
scheduleLoadBackendGroupedRows()
|
scheduleLoadBackendGroupedRows()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(detailMainGroupOptions, options => {
|
||||||
|
if (!options.length) {
|
||||||
|
selectedDetailMainGroup.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedDetailMainGroup.value || !options.some(option => option.value === selectedDetailMainGroup.value)) {
|
||||||
|
selectedDetailMainGroup.value = options[0].value
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(selectedDetailMainGroup, () => {
|
||||||
|
if (activeTab.value === 'product_detail') {
|
||||||
|
expandedGroups.value = Object.fromEntries(
|
||||||
|
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith('tab:product_detail|'))
|
||||||
|
)
|
||||||
|
backendGroupedRows.value = {
|
||||||
|
...backendGroupedRows.value,
|
||||||
|
product_detail: []
|
||||||
|
}
|
||||||
|
scheduleLoadBackendGroupedRows()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(productTableRows, tableRows => {
|
watch(productTableRows, tableRows => {
|
||||||
if (activeTab.value !== 'products') return
|
if (!isProductKpiTab.value) return
|
||||||
const imageRows = tableRows
|
const sourceRows = activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : tableRows
|
||||||
|
const imageRows = sourceRows
|
||||||
.filter(row => !row.__group || groupShowsImage(row))
|
.filter(row => !row.__group || groupShowsImage(row))
|
||||||
.slice(0, 240)
|
.slice(0, 240)
|
||||||
void primeProductImages(imageRows)
|
void primeProductImages(imageRows)
|
||||||
@@ -4308,6 +4495,49 @@ onMounted(reload)
|
|||||||
opacity: 0.62;
|
opacity: 0.62;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-group-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #d7dde5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid #c9d2dd;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f7f9fb;
|
||||||
|
color: #26384c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button small {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button.active {
|
||||||
|
border-color: #1d4ed8;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #173a75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-card {
|
.metric-card {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
}
|
}
|
||||||
@@ -4417,18 +4647,26 @@ onMounted(reload)
|
|||||||
.performance-table :deep(.q-table tbody td .q-field),
|
.performance-table :deep(.q-table tbody td .q-field),
|
||||||
.performance-table :deep(.q-table tbody td input),
|
.performance-table :deep(.q-table tbody td input),
|
||||||
.performance-table :deep(.q-table tbody td textarea) {
|
.performance-table :deep(.q-table tbody td textarea) {
|
||||||
min-width: 72px;
|
min-width: 54px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody td .q-field__control),
|
.performance-table :deep(.q-table tbody td .q-field__control),
|
||||||
.performance-table :deep(.q-table tbody td .q-field__native),
|
.performance-table :deep(.q-table tbody td .q-field__native),
|
||||||
.performance-table :deep(.q-table tbody td .q-field__input) {
|
.performance-table :deep(.q-table tbody td .q-field__input) {
|
||||||
min-height: 26px;
|
min-height: 24px;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.performance-table :deep(.q-table th.color-yaka-cell),
|
||||||
|
.performance-table :deep(.q-table td.color-yaka-cell) {
|
||||||
|
width: 132px;
|
||||||
|
min-width: 132px;
|
||||||
|
max-width: 150px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table th:first-child),
|
.performance-table :deep(.q-table th:first-child),
|
||||||
.performance-table :deep(.q-table td:first-child) {
|
.performance-table :deep(.q-table td:first-child) {
|
||||||
border-left: 1px solid #d5dce5;
|
border-left: 1px solid #d5dce5;
|
||||||
@@ -4778,39 +5016,39 @@ onMounted(reload)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-0 td) {
|
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-0 td) {
|
||||||
background: #2f5f8f;
|
background: #123b63;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #244b72;
|
border-color: #0b2845;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-1 td) {
|
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-1 td) {
|
||||||
background: #4f7fad;
|
background: #1f6f54;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #426d96;
|
border-color: #18563f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-2 td) {
|
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-2 td) {
|
||||||
background: #d8e6f4;
|
background: #ffe2a8;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
border-color: #b8cce0;
|
border-color: #d7a84d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-3 td) {
|
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-3 td) {
|
||||||
background: #e6eef7;
|
background: #d8f0dc;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
border-color: #c4d2e2;
|
border-color: #9bc7a3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-4 td) {
|
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-4 td) {
|
||||||
background: #eef4fa;
|
background: #e7ecff;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
border-color: #cbd8e6;
|
border-color: #b8c2ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-5 td) {
|
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-5 td) {
|
||||||
background: #f4f8fc;
|
background: #f2f3f5;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
border-color: #d5dce5;
|
border-color: #c4c9d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-table :deep(.q-table tbody tr.group-row td span),
|
.performance-table :deep(.q-table tbody tr.group-row td span),
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
groupedCacheKey (params = {}) {
|
groupedCacheKey (params = {}) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
mode: String(params.mode || ''),
|
mode: String(params.mode || ''),
|
||||||
|
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
|
||||||
groupLevels: normalizedList(params.groupLevels),
|
groupLevels: normalizedList(params.groupLevels),
|
||||||
expandedKeys: normalizedList(params.expandedKeys, true),
|
expandedKeys: normalizedList(params.expandedKeys, true),
|
||||||
limit: Number(params.limit || 0)
|
limit: Number(params.limit || 0)
|
||||||
@@ -104,13 +105,15 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
})
|
})
|
||||||
const request = api.post('/pricing/product-performance/grouped', {
|
const request = api.post('/pricing/product-performance/grouped', {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
||||||
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
||||||
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
||||||
limit: params.limit
|
limit: params.limit
|
||||||
}, {
|
}, {
|
||||||
params: {
|
params: {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
limit: params.limit
|
limit: params.limit,
|
||||||
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || ''
|
||||||
},
|
},
|
||||||
timeout: 90000
|
timeout: 90000
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
|
|||||||
Reference in New Issue
Block a user