From 1448dab9d77aa17d302fb94ed413795e03ba50fc Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Mon, 6 Jul 2026 17:01:39 +0300 Subject: [PATCH] Fix product performance grouped JSON build --- svc/routes/product_performance_excel.go | 80 +++++++++++++++---- .../pages/ProductPerformanceProfitability.vue | 26 +++--- 2 files changed, 79 insertions(+), 27 deletions(-) diff --git a/svc/routes/product_performance_excel.go b/svc/routes/product_performance_excel.go index 3fd9805..4d92275 100644 --- a/svc/routes/product_performance_excel.go +++ b/svc/routes/product_performance_excel.go @@ -51,6 +51,10 @@ type productPerformanceExcelVariant struct { UrunAnaGrubu string UrunAltGrubu string StockQty float64 + AvgStock90 float64 + AvgStock180 float64 + AvgStock365 float64 + AvgStockTotal float64 BasePriceUSD float64 CostPriceUSD float64 @@ -188,6 +192,10 @@ func (v *productPerformanceExcelVariant) addProductRow(row models.ProductPerform v.SalesUSD90 += row.SalesUSD90 v.SalesUSD180 += row.SalesUSD180 v.SalesUSD365 += row.SalesUSD365 + v.AvgStock90 = productPerformanceExcelFirstNonZero(v.AvgStock90, row.AvgStock90) + v.AvgStock180 = productPerformanceExcelFirstNonZero(v.AvgStock180, row.AvgStock180) + v.AvgStock365 = productPerformanceExcelFirstNonZero(v.AvgStock365, row.AvgStock365) + v.AvgStockTotal = productPerformanceExcelFirstNonZero(v.AvgStockTotal, row.AvgStockTotal) v.SalesQtyTotalProduct += row.SalesQtyTotal v.SalesUSDTotalProduct += row.SalesUSDTotal v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90) @@ -407,9 +415,9 @@ func productPerformanceExcelPeriodColumns(label, suffix string, values func(*pro qty, usd, _, _ := values(v) return productPerformanceExcelAvgPrice(usd, qty) }), - numberExcelColumn(label+" Stok Devir Hızı", "number", func(v *productPerformanceExcelVariant) any { + numberExcelColumn(label+" Yıllık Stok Devir", "number", func(v *productPerformanceExcelVariant) any { qty, _, _, _ := values(v) - return productPerformanceExcelStockTurnover(qty, v.StockQty) + return productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix)) }), numberExcelColumn(label+" Taban Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.BasePriceUSD }), numberExcelColumn(label+" Çıplak Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.CostPriceUSD }), @@ -439,7 +447,7 @@ func productPerformanceExcelPeriodColumns(label, suffix string, values func(*pro }), numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any { qty, usd, markets, customers := values(v) - return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, v.StockQty), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD)) + return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix)), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD)) }), } } @@ -657,13 +665,13 @@ func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sort case "avg_price_usd_total": return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty()) case "stock_turnover_90d": - return productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty) + return productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d")) case "stock_turnover_180d": - return productPerformanceExcelStockTurnover(row.SalesQty180, row.StockQty) + return productPerformanceExcelStockTurnover(row.SalesQty180, productPerformanceExcelAvgStock(row, "180d"), productPerformanceExcelPeriodDays(row, "180d")) case "stock_turnover_365d": - return productPerformanceExcelStockTurnover(row.SalesQty365, row.StockQty) + return productPerformanceExcelStockTurnover(row.SalesQty365, productPerformanceExcelAvgStock(row, "365d"), productPerformanceExcelPeriodDays(row, "365d")) case "stock_turnover_total": - return productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty) + return productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")) case "base_price_usd": return row.BasePriceUSD case "cost_price_usd": @@ -677,9 +685,9 @@ func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sort case "customer_count_total": return float64(row.CustomerCountTotal) case "performance_score", "performance_score_90d": - return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD)) + return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d")), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD)) case "performance_score_total": - return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) + return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) default: return 0 } @@ -795,7 +803,7 @@ func productPerformanceExcelStatus(row *productPerformanceExcelVariant) string { return "Stok Riski" case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0: return "Fiyat Baskısı" - case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) >= 70: + case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) >= 70: return "Yıldız Ürün" default: return productPerformanceExcelBucketLabel(row.PerformanceBucket) @@ -881,11 +889,55 @@ func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 { return salesUSD / qty } -func productPerformanceExcelStockTurnover(salesQty, stockQty float64) float64 { - if stockQty <= 0 { +const productPerformanceExcelStockTurnoverYearDays = 360.0 + +func productPerformanceExcelAvgStock(row *productPerformanceExcelVariant, suffix string) float64 { + switch suffix { + case "90d": + return productPerformanceExcelFirstNonZero(row.AvgStock90, row.StockQty) + case "180d": + return productPerformanceExcelFirstNonZero(row.AvgStock180, row.StockQty) + case "365d": + return productPerformanceExcelFirstNonZero(row.AvgStock365, row.StockQty) + case "total": + return productPerformanceExcelFirstNonZero(row.AvgStockTotal, row.StockQty) + default: + return row.StockQty + } +} + +func productPerformanceExcelPeriodDays(row *productPerformanceExcelVariant, suffix string) float64 { + switch suffix { + case "90d": + return 90 + case "180d": + return 180 + case "365d": + return 360 + case "total": + end, err := time.Parse("2006-01-02", strings.TrimSpace(row.KpiDate)) + if err != nil { + return 0 + } + start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + if end.Before(start) { + return 0 + } + return end.Sub(start).Hours()/24 + 1 + default: return 0 } - return salesQty / stockQty +} + +func productPerformanceExcelStockTurnover(salesQty, avgStock, periodDays float64) float64 { + if salesQty <= 0 || avgStock <= 0 { + return 0 + } + raw := salesQty / avgStock + if periodDays <= 0 { + return raw + } + return raw * productPerformanceExcelStockTurnoverYearDays / periodDays } func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 { @@ -908,7 +960,7 @@ func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, } revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix)) score := 0.30*productPerformanceExcelMarginScore(margin) + - 0.20*productPerformanceExcelRatioScore(stockTurnover, 1.50) + + 0.20*productPerformanceExcelRatioScore(stockTurnover, 4) + 0.20*revenueScore + 0.15*productPerformanceExcelRatioScore(marketCount, 8) + 0.15*productPerformanceExcelRatioScore(customerCount, 25) diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index b6f1f32..dd67a2c 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -1817,10 +1817,10 @@ const columns = [ { name: 'stock_days_180d', label: '180G Stok Gün', field: row => formatNumber(row.stock_days_180d, 1), align: 'right', sortable: true }, { name: 'stock_days_365d', label: '360G Stok Gün', field: row => formatNumber(row.stock_days_365d, 1), align: 'right', sortable: true }, { name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true }, - { name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, - { name: 'stock_turnover_180d', label: '180G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true }, - { name: 'stock_turnover_365d', label: '360G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true }, - { name: 'stock_turnover_total', label: 'Genel Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_90d', label: '90G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_180d', label: '180G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_365d', label: '360G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_total', label: 'Genel Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true }, { name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' }, { name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' }, { name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' }, @@ -2233,7 +2233,7 @@ const idleColumns = [ { name: 'idle_cost_usd', label: 'Stok Maliyeti USD', field: 'idle_cost_usd', align: 'right', sortable: true }, { name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true }, { name: 'stock_days_90d', label: 'Stok Gün', field: row => formatNumber(row.stock_days_90d, 1), align: 'right', sortable: true }, - { name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_90d', label: '90G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, { name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' }, { name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' } ] @@ -2316,13 +2316,13 @@ const salesBreakdownColumns = [ { name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true }, { name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true }, { name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: 'avg_price_usd_90d', align: 'right', sortable: true }, - { name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_90d', label: '90G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, { name: 'customer_count_180d', label: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true }, { name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true }, { name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true }, { name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true }, { name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true }, - { name: 'stock_turnover_180d', label: '180G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_180d', label: '180G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true }, { name: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true }, { name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true }, { name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true }, @@ -2333,7 +2333,7 @@ const salesBreakdownColumns = [ { name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true }, { name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true }, { name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true }, - { name: 'stock_turnover_365d', label: '360G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_365d', label: '360G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true }, { name: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true }, { name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true }, { name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true }, @@ -2346,7 +2346,7 @@ const salesBreakdownColumns = [ { name: 'sales_qty_total', label: 'Genel Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true }, { name: 'sales_usd_total', label: 'Genel USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'avg_price_usd_total', label: 'Genel Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true }, - { name: 'stock_turnover_total', label: 'Genel Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_total', label: 'Genel Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true }, { name: 'base_price_usd_total', label: 'Genel Taban', field: row => formatMoney(row.base_price_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'cost_price_usd_total', label: 'Genel Çıplak', field: row => formatMoney(row.cost_price_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'gross_profit_base_usd_total', label: 'Genel Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true }, @@ -2440,10 +2440,10 @@ const metricLabelOverrides = { customer_count_90d: '90G Tekil Müşteri', market_count_total: 'Genel Tekil Piyasa', customer_count_total: 'Genel Tekil Müşteri', - stock_turnover_90d: '90G Stok Devir Hızı', - stock_turnover_180d: '180G Stok Devir Hızı', - stock_turnover_365d: '360G Stok Devir Hızı', - stock_turnover_total: 'Genel Stok Devir Hızı', + stock_turnover_90d: '90G Yıllık Stok Devir', + stock_turnover_180d: '180G Yıllık Stok Devir', + stock_turnover_365d: '360G Yıllık Stok Devir', + stock_turnover_total: 'Genel Yıllık Stok Devir', sales_index_90d: '90G Endeks', sales_index_total: 'Genel Endeks', stock_days_90d: '90G Stok Gün',