diff --git a/svc/main.go b/svc/main.go index d0d05c0..7179747 100644 --- a/svc/main.go +++ b/svc/main.go @@ -939,6 +939,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "pricing", "view", wrapV3(routes.GetProductPerformanceHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/pricing/product-performance/export-excel", "POST", + "pricing", "view", + wrapV3(routes.ExportProductPerformanceExcelHandler(pgDB)), + ) bindV3(r, pgDB, "/api/pricing/product-performance/summary", "GET", "pricing", "view", diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index bf82094..232a780 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -3937,8 +3937,8 @@ func ListProductPerformanceGroupedFilterOptions(ctx context.Context, pg *sql.DB, if err := EnsureProductPerformanceTables(pg); err != nil { return nil, err } - if req.Limit <= 0 || req.Limit > 5000 { - req.Limit = 5000 + if req.Limit <= 0 || req.Limit > 50000 { + req.Limit = 50000 } levels := sanitizeProductPerformanceGroupLevels(req.GroupLevels) if len(levels) == 0 { @@ -4460,22 +4460,22 @@ FROM ( 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(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 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 + WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END) > 0 + THEN SUM(CASE WHEN sales_qty_total > 0 THEN cost_price_usd * sales_qty_total ELSE 0 END) + / NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0) + ELSE COALESCE(AVG(NULLIF(cost_price_usd,0)),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 + WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END) > 0 + THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_usd * sales_qty_total ELSE 0 END) + / NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0) + ELSE COALESCE(AVG(NULLIF(base_price_usd,0)),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 + WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END) > 0 + THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_try * sales_qty_total ELSE 0 END) + / NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0) + ELSE COALESCE(AVG(NULLIF(base_price_try,0)),0) END AS base_price_try, COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d, COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d, @@ -5275,7 +5275,7 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) { n.Avg[key] = state } number := floatFromAny(value) - weight := floatFromMap(row, productPerformanceMetricWeightField(key)) + weight := productPerformanceMetricWeight(row, key) if weight > 0 { state.Weighted += number * weight state.Weight += weight @@ -5722,15 +5722,33 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 { out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty } - out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d")) - out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d")) - out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d")) - out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total")) - out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d") - out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d") - out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d") - out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total") - out["performance_score"] = productPerformanceGroupScore(out, groupField) + if _, ok := out["customer_score_90d"]; !ok { + out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d")) + } + if _, ok := out["customer_score_180d"]; !ok { + out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d")) + } + if _, ok := out["customer_score_365d"]; !ok { + out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d")) + } + if _, ok := out["customer_score_total"]; !ok { + out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total")) + } + if _, ok := out["performance_score_90d"]; !ok { + out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d") + } + if _, ok := out["performance_score_180d"]; !ok { + out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d") + } + if _, ok := out["performance_score_365d"]; !ok { + out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d") + } + if _, ok := out["performance_score_total"]; !ok { + out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total") + } + if _, ok := out["performance_score"]; !ok { + out["performance_score"] = productPerformanceGroupScore(out, groupField) + } if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 { score := productPerformanceOrderGroupScore(out) out["performance_score_90d"] = score @@ -6087,6 +6105,9 @@ func shouldAverageProductPerformanceField(field string) bool { } func productPerformanceMetricWeightField(field string) string { + if productPerformanceUsesCostWeight(field) { + return "__product_cost_weight" + } if strings.Contains(field, "_180d") { return "sales_qty_180d" } @@ -6105,11 +6126,44 @@ func productPerformanceMetricWeightField(field string) string { return "sales_qty_90d" } +func productPerformanceUsesCostWeight(field string) bool { + if !(strings.HasPrefix(field, "base_price") || strings.HasPrefix(field, "cost_price")) { + return false + } + return !strings.Contains(field, "_90d") && + !strings.Contains(field, "_180d") && + !strings.Contains(field, "_365d") && + !strings.Contains(field, "_total") +} + +func productPerformanceMetricWeight(row map[string]any, field string) float64 { + weightField := productPerformanceMetricWeightField(field) + if weightField == "__product_cost_weight" { + return productPerformanceCostWeight(row) + } + return floatFromMap(row, weightField) +} + +func productPerformanceCostWeight(row map[string]any) float64 { + if weight := floatFromMap(row, "sales_qty_total"); weight > 0 { + return weight + } + for _, field := range []string{"sales_qty_365d", "sales_qty_180d", "sales_qty_90d"} { + if weight := floatFromMap(row, field); weight > 0 { + return weight + } + } + return 0 +} + func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qtyField string) float64 { var weighted, qty float64 for _, row := range rows { value := floatFromMap(row, valueField) weight := floatFromMap(row, qtyField) + if qtyField == "__product_cost_weight" { + weight = productPerformanceCostWeight(row) + } if weight > 0 { weighted += value * weight qty += weight @@ -6118,9 +6172,50 @@ func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qt if qty > 0 { return weighted / qty } + if qtyField == "__product_cost_weight" { + return averageProductPerformanceDistinctVariantField(rows, valueField) + } return 0 } +func averageProductPerformanceDistinctVariantField(rows []map[string]any, valueField string) float64 { + seen := map[string]bool{} + sum := 0.0 + count := 0.0 + hasKey := false + for _, row := range rows { + key := productPerformanceMapVariantKey(row) + if key == "" { + continue + } + hasKey = true + if seen[key] { + continue + } + seen[key] = true + value := floatFromMap(row, valueField) + if value <= 0 { + continue + } + sum += value + count++ + } + if !hasKey { + for _, row := range rows { + value := floatFromMap(row, valueField) + if value <= 0 { + continue + } + sum += value + count++ + } + } + if count <= 0 { + return 0 + } + return sum / count +} + func dominantProductPerformanceValue(rows []map[string]any, field string) string { counts := map[string]int{} for _, row := range rows { diff --git a/svc/routes/product_images.go b/svc/routes/product_images.go index 9058e51..e9bc8ae 100644 --- a/svc/routes/product_images.go +++ b/svc/routes/product_images.go @@ -17,15 +17,16 @@ import ( ) type ProductImageItem struct { - ID int64 `json:"id"` - FileName string `json:"file_name"` - FileSize int64 `json:"file_size"` - Storage string `json:"storage_path"` - ContentURL string `json:"content_url"` - UUID string `json:"uuid,omitempty"` - ThumbURL string `json:"thumb_url,omitempty"` - FullURL string `json:"full_url,omitempty"` - StoredInDB bool `json:"stored_in_db,omitempty"` + ID int64 `json:"id"` + FileName string `json:"file_name"` + FileSize int64 `json:"file_size"` + Storage string `json:"storage_path"` + ContentURL string `json:"content_url"` + UUID string `json:"uuid,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` + FullURL string `json:"full_url,omitempty"` + StoredInDB bool `json:"stored_in_db,omitempty"` + HasDBContent bool `json:"-"` } type ProductImageBatchItem struct { @@ -141,14 +142,68 @@ func enrichProductImageItem(it *ProductImageItem) { if it == nil { return } - if it.ID > 0 { - it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) - } if u := extractImageUUID(it.Storage, it.FileName); u != "" { it.UUID = u - it.ThumbURL = "/uploads/image/t300/" + u + ".jpg" - it.FullURL = "/uploads/image/" + u + ".jpg" + it.ThumbURL = productImagePublicURLIfExists("uploads/image/t300/" + u + ".jpg") + it.FullURL = productImagePublicURLIfExists("uploads/image/" + u + ".jpg") } + if it.FullURL == "" { + it.FullURL = productImagePublicURLIfExists(it.Storage) + } + if it.FullURL == "" { + it.FullURL = productImagePublicURLIfExists(it.FileName) + } + if it.ID > 0 && (it.HasDBContent || productImageStoredFileExists(it.Storage, it.FileName)) { + it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) + } +} + +func productImageItemHasURL(it *ProductImageItem) bool { + return it != nil && (it.ThumbURL != "" || it.FullURL != "" || it.ContentURL != "") +} + +func productImageStoredFileExists(storagePath, fileName string) bool { + if resolved, _ := resolveStoragePath(storagePath); resolved != "" { + return true + } + if resolved, _ := resolveStoragePath(fileName); resolved != "" { + return true + } + return false +} + +func productImagePublicURLIfExists(storagePath string) string { + raw := strings.TrimSpace(storagePath) + if raw == "" { + return "" + } + resolved, _ := resolveStoragePath(raw) + if resolved == "" { + return "" + } + + normalized := strings.ReplaceAll(raw, "\\", "/") + lower := strings.ToLower(normalized) + if idx := strings.Index(lower, "/uploads/"); idx >= 0 { + return normalized[idx:] + } + if strings.HasPrefix(lower, "uploads/") { + return "/" + normalized + } + + root := strings.TrimSpace(os.Getenv("BLOB_ROOT")) + if root == "" { + return "" + } + rel, err := filepath.Rel(root, resolved) + if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) { + return "" + } + rel = filepath.ToSlash(rel) + if strings.HasPrefix(strings.ToLower(rel), "uploads/") { + return "/" + rel + } + return "" } // POST /api/product-images/batch @@ -224,6 +279,7 @@ ranked AS ( COALESCE(b.file_size,0) AS file_size, COALESCE(b.storage_path,'') AS storage_path, COALESCE(b.stored_in_db,false) AS stored_in_db, + (COALESCE(octet_length(b.bin),0) > 0) AS has_db_content, ROW_NUMBER() OVER ( PARTITION BY mi.key ORDER BY @@ -241,9 +297,9 @@ ranked AS ( AND b.typ='img' AND b.src_id=mi.mmitem_id ) -SELECT key, id, file_name, file_size, storage_path, stored_in_db +SELECT key, id, file_name, file_size, storage_path, stored_in_db, has_db_content FROM ranked -WHERE rn <= 1 +WHERE rn <= 5 ORDER BY key, rn `, string(payload)) if err != nil { @@ -257,10 +313,13 @@ ORDER BY key, rn for rows.Next() { var key string var it ProductImageItem - if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB); err != nil { + if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB, &it.HasDBContent); err != nil { continue } enrichProductImageItem(&it) + if !productImageItemHasURL(&it) { + continue + } grouped[key] = append(grouped[key], it) } if err := rows.Err(); err != nil { @@ -351,7 +410,8 @@ SELECT COALESCE(file_name,'') AS file_name, COALESCE(file_size,0) AS file_size, COALESCE(storage_path,'') AS storage_path, - COALESCE(stored_in_db,false) AS stored_in_db + COALESCE(stored_in_db,false) AS stored_in_db, + (COALESCE(octet_length(bin),0) > 0) AS has_db_content FROM dfblob WHERE typ='img' AND src_table='mmitem' @@ -382,11 +442,13 @@ ORDER BY items := make([]ProductImageItem, 0, 16) for rows.Next() { var it ProductImageItem - if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB); err != nil { + if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB, &it.HasDBContent); err != nil { continue } enrichProductImageItem(&it) - items = append(items, it) + if productImageItemHasURL(&it) { + items = append(items, it) + } } if err := rows.Err(); err != nil { return nil, err diff --git a/svc/routes/product_performance.go b/svc/routes/product_performance.go index f37ef12..c43f5e4 100644 --- a/svc/routes/product_performance.go +++ b/svc/routes/product_performance.go @@ -370,7 +370,7 @@ func GetProductPerformanceGroupedFilterOptionsHandler(pg *sql.DB) http.HandlerFu mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu")) groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels")) fields := splitCSVQuery(r.URL.Query().Get("fields")) - limit := intQuery(r, "limit", 5000) + limit := intQuery(r, "limit", 50000) if r.Method == http.MethodPost { var body struct { diff --git a/svc/routes/product_performance_excel.go b/svc/routes/product_performance_excel.go new file mode 100644 index 0000000..ea932f4 --- /dev/null +++ b/svc/routes/product_performance_excel.go @@ -0,0 +1,929 @@ +package routes + +import ( + "bssapp-backend/models" + "bssapp-backend/queries" + "bssapp-backend/utils" + "context" + "database/sql" + "encoding/json" + "fmt" + "math" + "net/http" + "sort" + "strings" + "time" + + "github.com/xuri/excelize/v2" +) + +type productPerformanceExcelExportRequest struct { + Filters map[string][]string `json:"filters"` + SortBy string `json:"sort_by"` + Descending *bool `json:"descending"` +} + +type productPerformanceExcelColumn struct { + Header string + Kind string + Value func(models.ProductPerformanceRow, models.ProductPerformanceGeneralRow, productPerformanceExcelStats) any +} + +type productPerformanceExcelStats struct { + AvgSalesUSD90 float64 + AvgSalesUSD180 float64 + AvgSalesUSD365 float64 + AvgSalesUSDTotal float64 +} + +func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + traceID := utils.TraceIDFromRequest(r) + ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second) + defer cancel() + + var req productPerformanceExcelExportRequest + if r.Method == http.MethodPost { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "gecersiz excel istegi: "+err.Error(), http.StatusBadRequest) + return + } + } + + rows, _, err := queries.ListProductPerformance(ctx, pg, queries.ProductPerformanceFilters{ + Limit: 50000, + Page: 1, + SortBy: "product_code", + Descending: false, + }) + if err != nil { + http.Error(w, "urun performans excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError) + return + } + + generalRows, err := queries.ListProductPerformanceGeneral(ctx, pg, 50000) + if err != nil { + http.Error(w, "urun performans genel excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError) + return + } + generalByKey := productPerformanceExcelGeneralMap(generalRows) + + rows = filterProductPerformanceExcelRows(rows, generalByKey, req.Filters) + sortProductPerformanceExcelRows(rows, generalByKey, req.SortBy, req.Descending == nil || *req.Descending) + stats := productPerformanceExcelStatsFor(rows, generalByKey) + + file, err := buildProductPerformanceExcelFile(rows, generalByKey, stats) + if err != nil { + http.Error(w, "urun performans excel dosyasi hazirlanamadi: "+err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + buf, err := file.WriteToBuffer() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + filename := fmt.Sprintf("product_performance_color_yaka_%s.xlsx", time.Now().Format("20060102_150405")) + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") + w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes()))) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(buf.Bytes()) + } +} + +func buildProductPerformanceExcelFile(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) (*excelize.File, error) { + f := excelize.NewFile() + sheet := "RenkYaka" + f.SetSheetName("Sheet1", sheet) + + columns := productPerformanceExcelColumns() + headerStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"1F4E78"}}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true}, + }) + textStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 49}) + intStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 3}) + decimalStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 4}) + percentStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 10}) + + for i, col := range columns { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + _ = f.SetCellStr(sheet, cell, col.Header) + } + if len(columns) > 0 { + lastHeader, _ := excelize.CoordinatesToCellName(len(columns), 1) + _ = f.SetCellStyle(sheet, "A1", lastHeader, headerStyle) + } + + for rowIndex, row := range rows { + excelRow := rowIndex + 2 + general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] + for colIndex, col := range columns { + cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow) + value := col.Value(row, general, stats) + switch col.Kind { + case "text": + _ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value))) + default: + _ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value)) + } + } + } + + lastRow := len(rows) + 1 + if len(columns) > 0 { + filterLastRow := lastRow + if filterLastRow < 2 { + filterLastRow = 2 + } + lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), filterLastRow) + _ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{}) + } + for i, col := range columns { + colName, _ := excelize.ColumnNumberToName(i + 1) + width := productPerformanceExcelColumnWidth(col.Header, col.Kind) + _ = f.SetColWidth(sheet, colName, colName, width) + if lastRow >= 2 { + style := decimalStyle + switch col.Kind { + case "text": + style = textStyle + case "int": + style = intStyle + case "percent": + style = percentStyle + } + _ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style) + } + } + _ = f.SetPanes(sheet, &excelize.Panes{ + Freeze: true, + Split: false, + XSplit: 0, + YSplit: 1, + TopLeftCell: "A2", + ActivePane: "bottomLeft", + }) + return f, nil +} + +func productPerformanceExcelColumns() []productPerformanceExcelColumn { + return []productPerformanceExcelColumn{ + {Header: "KPI Tarihi", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.KpiDate + }}, + {Header: "Ürün İlk Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelCleanOptional(r.UrunIlkGrubu) + }}, + {Header: "Askılı/Yan", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelCleanOptional(r.AskiliYan) + }}, + {Header: "Kategori", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.Kategori + }}, + {Header: "Ürün Ana Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.UrunAnaGrubu + }}, + {Header: "Ürün Alt Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.UrunAltGrubu + }}, + {Header: "Ürün", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.ProductCode + }}, + {Header: "Açıklama", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.ItemDescription + }}, + {Header: "Renk", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.ColorCode + }}, + {Header: "Renk Açıklama", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.ColorDescription + }}, + {Header: "Yaka", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.YakaKodu + }}, + {Header: "Renk/Yaka", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelColorYaka(r) + }}, + {Header: "Toplam Stok", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.StockQty + }}, + + {Header: "90G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.SalesQty90 + }}, + {Header: "90G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.SalesUSD90 + }}, + {Header: "90G Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelAvgPrice(r.SalesUSD90, r.SalesQty90) + }}, + {Header: "90G Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.StockDays90 + }}, + {Header: "90G Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(r.StockTurnover90, productPerformanceExcelStockTurnover(r.SalesQty90, r.StockQty)) + }}, + {Header: "90G Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.BasePriceUSD + }}, + {Header: "90G Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.CostPriceUSD + }}, + {Header: "90G Ağ. Ort. Birim Taban K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelUnitProfit(r.SalesUSD90, r.SalesQty90, r.BasePriceUSD) + }}, + {Header: "90G Ağ. Ort. Birim Çıplak K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelUnitProfit(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD) + }}, + {Header: "90G Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(r.SalesUSD90, r.SalesQty90, r.BasePriceUSD) + }}, + {Header: "90G Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD) + }}, + {Header: "90G Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(r.SalesUSD90, r.SalesQty90, r.BasePriceUSD) + }}, + {Header: "90G Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD) + }}, + {Header: "90G Tekil Piyasa", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.MarketCount90 + }}, + {Header: "90G Tekil Müşteri", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.CustomerCount90 + }}, + {Header: "90G Endeks", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(r.SalesIndex90, productPerformanceExcelRelativeIndex(r.SalesUSD90, stats.AvgSalesUSD90)) + }}, + {Header: "90G Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { + if strings.TrimSpace(r.PerformanceBucket) != "" { + return r.PerformanceScore + } + return productPerformanceExcelProductScore("90d", r.SalesUSD90, productPerformanceExcelRelativeIndex(r.SalesUSD90, stats.AvgSalesUSD90), productPerformanceExcelMargin(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD), productPerformanceExcelStockTurnover(r.SalesQty90, r.StockQty), float64(r.MarketCount90), float64(r.CustomerCount90)) + }}, + + {Header: "180G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.SalesQty180 + }}, + {Header: "180G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.SalesUSD180 + }}, + {Header: "180G Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelAvgPrice(r.SalesUSD180, r.SalesQty180) + }}, + {Header: "180G Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.StockDays180 + }}, + {Header: "180G Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(r.StockTurnover180, productPerformanceExcelStockTurnover(r.SalesQty180, r.StockQty)) + }}, + {Header: "180G Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.BasePriceUSD + }}, + {Header: "180G Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.CostPriceUSD + }}, + {Header: "180G Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(r.SalesUSD180, r.SalesQty180, r.BasePriceUSD) + }}, + {Header: "180G Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(r.SalesUSD180, r.SalesQty180, r.CostPriceUSD) + }}, + {Header: "180G Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(r.SalesUSD180, r.SalesQty180, r.BasePriceUSD) + }}, + {Header: "180G Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(r.SalesUSD180, r.SalesQty180, r.CostPriceUSD) + }}, + {Header: "180G Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { + return productPerformanceExcelProductScore("180d", r.SalesUSD180, productPerformanceExcelRelativeIndex(r.SalesUSD180, stats.AvgSalesUSD180), productPerformanceExcelMargin(r.SalesUSD180, r.SalesQty180, r.CostPriceUSD), productPerformanceExcelStockTurnover(r.SalesQty180, r.StockQty), productPerformanceExcelFirstNonZero(float64(g.MarketCountTotal), float64(r.MarketCount90)), productPerformanceExcelFirstNonZero(float64(g.CustomerCountTotal), float64(r.CustomerCount90))) + }}, + + {Header: "360G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.SalesQty365 + }}, + {Header: "360G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.SalesUSD365 + }}, + {Header: "360G Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelAvgPrice(r.SalesUSD365, r.SalesQty365) + }}, + {Header: "360G Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.StockDays365 + }}, + {Header: "360G Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(r.StockTurnover365, productPerformanceExcelStockTurnover(r.SalesQty365, r.StockQty)) + }}, + {Header: "360G Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.BasePriceUSD + }}, + {Header: "360G Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.CostPriceUSD + }}, + {Header: "360G Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(r.SalesUSD365, r.SalesQty365, r.BasePriceUSD) + }}, + {Header: "360G Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(r.SalesUSD365, r.SalesQty365, r.CostPriceUSD) + }}, + {Header: "360G Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(r.SalesUSD365, r.SalesQty365, r.BasePriceUSD) + }}, + {Header: "360G Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(r.SalesUSD365, r.SalesQty365, r.CostPriceUSD) + }}, + {Header: "360G Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { + return productPerformanceExcelProductScore("365d", r.SalesUSD365, productPerformanceExcelRelativeIndex(r.SalesUSD365, stats.AvgSalesUSD365), productPerformanceExcelMargin(r.SalesUSD365, r.SalesQty365, r.CostPriceUSD), productPerformanceExcelStockTurnover(r.SalesQty365, r.StockQty), productPerformanceExcelFirstNonZero(float64(g.MarketCountTotal), float64(r.MarketCount90)), productPerformanceExcelFirstNonZero(float64(g.CustomerCountTotal), float64(r.CustomerCount90))) + }}, + + {Header: "Genel Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelTotalSalesQty(r, g) + }}, + {Header: "Genel Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelTotalSalesUSD(r, g) + }}, + {Header: "Genel Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(g.AvgPriceUSDTotal, productPerformanceExcelAvgPrice(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g))) + }}, + {Header: "Genel Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(g.StockDaysTotal, r.StockDaysTotal) + }}, + {Header: "Genel Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(r.StockTurnoverTotal, productPerformanceExcelStockTurnover(productPerformanceExcelTotalSalesQty(r, g), r.StockQty)) + }}, + {Header: "Genel Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(g.BasePriceUSD, r.BasePriceUSD) + }}, + {Header: "Genel Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD) + }}, + {Header: "Genel Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.BasePriceUSD, r.BasePriceUSD)) + }}, + {Header: "Genel Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelGrossProfit(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD)) + }}, + {Header: "Genel Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.BasePriceUSD, r.BasePriceUSD)) + }}, + {Header: "Genel Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelMargin(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD)) + }}, + {Header: "Genel Tekil Piyasa", Kind: "int", Value: func(_ models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return g.MarketCountTotal + }}, + {Header: "Genel Tekil Müşteri", Kind: "int", Value: func(_ models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return g.CustomerCountTotal + }}, + {Header: "Genel Fatura", Kind: "int", Value: func(_ models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return g.InvoiceCountTotal + }}, + {Header: "Genel Endeks", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonZero(g.SalesIndexTotal, productPerformanceExcelRelativeIndex(productPerformanceExcelTotalSalesUSD(r, g), stats.AvgSalesUSDTotal)) + }}, + {Header: "Genel Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { + if strings.TrimSpace(g.PerformanceBucket) != "" { + return g.PerformanceScore + } + return productPerformanceExcelProductScore("total", productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelRelativeIndex(productPerformanceExcelTotalSalesUSD(r, g), stats.AvgSalesUSDTotal), productPerformanceExcelMargin(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD)), productPerformanceExcelStockTurnover(productPerformanceExcelTotalSalesQty(r, g), r.StockQty), float64(g.MarketCountTotal), float64(g.CustomerCountTotal)) + }}, + {Header: "Durum", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelBucketLabel(productPerformanceExcelFirstNonEmpty(g.PerformanceBucket, r.PerformanceBucket)) + }}, + {Header: "Öneri", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonEmpty(g.Recommendation, r.Recommendation) + }}, + {Header: "Son Satış", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonEmpty(g.LastSaleDate, r.LastSaleDate) + }}, + {Header: "Son Ref", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return productPerformanceExcelFirstNonEmpty(g.LastRefNumber, r.LastRefNumber) + }}, + {Header: "Güncelleme", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { + return r.UpdatedAt + }}, + } +} + +func productPerformanceExcelGeneralMap(rows []models.ProductPerformanceGeneralRow) map[string]models.ProductPerformanceGeneralRow { + out := make(map[string]models.ProductPerformanceGeneralRow, len(rows)) + for _, row := range rows { + out[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] = row + } + return out +} + +func filterProductPerformanceExcelRows(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, filters map[string][]string) []models.ProductPerformanceRow { + if len(filters) == 0 { + return rows + } + out := make([]models.ProductPerformanceRow, 0, len(rows)) + for _, row := range rows { + general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] + if productPerformanceExcelRowMatchesFilters(row, general, filters) { + out = append(out, row) + } + } + return out +} + +func productPerformanceExcelRowMatchesFilters(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, filters map[string][]string) bool { + for field, values := range filters { + selected := productPerformanceExcelSelectedSet(values) + if len(selected) == 0 { + continue + } + matched := false + for _, candidate := range productPerformanceExcelFilterCandidates(row, general, field) { + if selected[productPerformanceExcelNormalize(candidate)] { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + +func productPerformanceExcelSelectedSet(values []string) map[string]bool { + out := make(map[string]bool, len(values)) + for _, value := range values { + value = productPerformanceExcelNormalize(value) + if value != "" { + out[value] = true + } + } + return out +} + +func productPerformanceExcelFilterCandidates(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, field string) []string { + switch strings.TrimSpace(field) { + case "product_code": + return []string{row.ProductCode} + case "item_description": + return []string{row.ItemDescription} + case "color_code": + return []string{row.ColorCode} + case "yaka_kodu": + return []string{row.YakaKodu} + case "color_yaka": + return []string{productPerformanceExcelColorYaka(row), row.ColorCode + "/" + row.YakaKodu} + case "kategori": + return []string{row.Kategori} + case "askili_yan": + return []string{productPerformanceExcelCleanOptional(row.AskiliYan)} + case "urun_ilk_grubu": + return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)} + case "urun_ana_grubu": + return []string{productPerformanceExcelFirstNonEmpty(general.UrunAnaGrubu, row.UrunAnaGrubu)} + case "urun_alt_grubu": + return []string{row.UrunAltGrubu} + case "market_key": + return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)} + case "performance_bucket": + bucket := productPerformanceExcelFirstNonEmpty(general.PerformanceBucket, row.PerformanceBucket) + return []string{bucket, productPerformanceExcelBucketLabel(bucket)} + default: + return nil + } +} + +func sortProductPerformanceExcelRows(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, sortBy string, desc bool) { + sortBy = strings.TrimSpace(sortBy) + if sortBy == "" || sortBy == "image" { + sortBy = "product_code" + desc = false + } + sort.SliceStable(rows, func(i, j int) bool { + left := rows[i] + right := rows[j] + leftGen := generalByKey[productPerformanceExcelVariantKey(left.ProductCode, left.ColorCode, left.YakaKodu)] + rightGen := generalByKey[productPerformanceExcelVariantKey(right.ProductCode, right.ColorCode, right.YakaKodu)] + cmp := productPerformanceExcelCompareSort(left, leftGen, right, rightGen, sortBy) + if cmp == 0 { + cmp = strings.Compare(productPerformanceExcelHierarchySortKey(left), productPerformanceExcelHierarchySortKey(right)) + } + if desc { + return cmp > 0 + } + return cmp < 0 + }) +} + +func productPerformanceExcelCompareSort(left models.ProductPerformanceRow, leftGen models.ProductPerformanceGeneralRow, right models.ProductPerformanceRow, rightGen models.ProductPerformanceGeneralRow, sortBy string) int { + leftNum, rightNum, numeric := productPerformanceExcelSortNumeric(left, leftGen, sortBy), productPerformanceExcelSortNumeric(right, rightGen, sortBy), productPerformanceExcelIsNumericSort(sortBy) + if numeric { + if leftNum < rightNum { + return -1 + } + if leftNum > rightNum { + return 1 + } + return 0 + } + leftText := productPerformanceExcelSortText(left, leftGen, sortBy) + rightText := productPerformanceExcelSortText(right, rightGen, sortBy) + return strings.Compare(productPerformanceExcelNormalize(leftText), productPerformanceExcelNormalize(rightText)) +} + +func productPerformanceExcelIsNumericSort(sortBy string) bool { + switch sortBy { + case "stock_qty", "sales_qty_90d", "sales_qty_180d", "sales_qty_365d", "sales_qty_total", + "sales_usd_90d", "sales_usd_180d", "sales_usd_365d", "sales_usd_total", + "avg_price_usd_90d", "avg_price_usd_180d", "avg_price_usd_365d", "avg_price_usd_total", + "stock_days_90d", "stock_days_180d", "stock_days_365d", "stock_days_total", + "stock_turnover_90d", "stock_turnover_180d", "stock_turnover_365d", "stock_turnover_total", + "base_price_usd", "cost_price_usd", "market_count_90d", "customer_count_90d", "market_count_total", "customer_count_total", + "sales_index_90d", "sales_index_total", "performance_score", "performance_score_90d", "performance_score_total": + return true + default: + return false + } +} + +func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, sortBy string) float64 { + switch sortBy { + case "stock_qty": + return row.StockQty + case "sales_qty_90d": + return row.SalesQty90 + case "sales_qty_180d": + return row.SalesQty180 + case "sales_qty_365d": + return row.SalesQty365 + case "sales_qty_total": + return productPerformanceExcelTotalSalesQty(row, general) + case "sales_usd_90d": + return row.SalesUSD90 + case "sales_usd_180d": + return row.SalesUSD180 + case "sales_usd_365d": + return row.SalesUSD365 + case "sales_usd_total": + return productPerformanceExcelTotalSalesUSD(row, general) + case "avg_price_usd_90d": + return productPerformanceExcelAvgPrice(row.SalesUSD90, row.SalesQty90) + case "avg_price_usd_180d": + return productPerformanceExcelAvgPrice(row.SalesUSD180, row.SalesQty180) + case "avg_price_usd_365d": + return productPerformanceExcelAvgPrice(row.SalesUSD365, row.SalesQty365) + case "avg_price_usd_total": + return productPerformanceExcelFirstNonZero(general.AvgPriceUSDTotal, productPerformanceExcelAvgPrice(productPerformanceExcelTotalSalesUSD(row, general), productPerformanceExcelTotalSalesQty(row, general))) + case "stock_days_90d": + return row.StockDays90 + case "stock_days_180d": + return row.StockDays180 + case "stock_days_365d": + return row.StockDays365 + case "stock_days_total": + return productPerformanceExcelFirstNonZero(general.StockDaysTotal, row.StockDaysTotal) + case "stock_turnover_90d": + return row.StockTurnover90 + case "stock_turnover_180d": + return row.StockTurnover180 + case "stock_turnover_365d": + return row.StockTurnover365 + case "stock_turnover_total": + return row.StockTurnoverTotal + case "base_price_usd": + return productPerformanceExcelFirstNonZero(general.BasePriceUSD, row.BasePriceUSD) + case "cost_price_usd": + return productPerformanceExcelFirstNonZero(general.CostPriceUSD, row.CostPriceUSD) + case "market_count_90d": + return float64(row.MarketCount90) + case "customer_count_90d": + return float64(row.CustomerCount90) + case "market_count_total": + return float64(general.MarketCountTotal) + case "customer_count_total": + return float64(general.CustomerCountTotal) + case "sales_index_90d": + return row.SalesIndex90 + case "sales_index_total": + return general.SalesIndexTotal + case "performance_score", "performance_score_90d": + return row.PerformanceScore + case "performance_score_total": + return general.PerformanceScore + default: + return 0 + } +} + +func productPerformanceExcelSortText(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, sortBy string) string { + switch sortBy { + case "product_code": + return row.ProductCode + case "color_yaka": + return productPerformanceExcelColorYaka(row) + case "color_code": + return row.ColorCode + case "yaka_kodu": + return row.YakaKodu + case "item_description": + return row.ItemDescription + case "kategori": + return row.Kategori + case "askili_yan": + return productPerformanceExcelCleanOptional(row.AskiliYan) + case "urun_ilk_grubu": + return productPerformanceExcelCleanOptional(row.UrunIlkGrubu) + case "urun_ana_grubu": + return productPerformanceExcelFirstNonEmpty(general.UrunAnaGrubu, row.UrunAnaGrubu) + case "urun_alt_grubu": + return row.UrunAltGrubu + case "market_key": + return productPerformanceExcelMarketName(row.MarketKey) + case "performance_bucket": + return productPerformanceExcelBucketLabel(productPerformanceExcelFirstNonEmpty(general.PerformanceBucket, row.PerformanceBucket)) + default: + return productPerformanceExcelHierarchySortKey(row) + } +} + +func productPerformanceExcelHierarchySortKey(row models.ProductPerformanceRow) string { + return strings.Join([]string{ + productPerformanceExcelCleanOptional(row.UrunIlkGrubu), + productPerformanceExcelCleanOptional(row.AskiliYan), + row.Kategori, + row.UrunAnaGrubu, + row.UrunAltGrubu, + row.ProductCode, + row.ColorCode, + row.YakaKodu, + }, "|") +} + +func productPerformanceExcelStatsFor(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow) productPerformanceExcelStats { + var stats productPerformanceExcelStats + var c90, c180, c365, cTotal float64 + for _, row := range rows { + general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] + if row.SalesUSD90 > 0 { + stats.AvgSalesUSD90 += row.SalesUSD90 + c90++ + } + if row.SalesUSD180 > 0 { + stats.AvgSalesUSD180 += row.SalesUSD180 + c180++ + } + if row.SalesUSD365 > 0 { + stats.AvgSalesUSD365 += row.SalesUSD365 + c365++ + } + if total := productPerformanceExcelTotalSalesUSD(row, general); total > 0 { + stats.AvgSalesUSDTotal += total + cTotal++ + } + } + if c90 > 0 { + stats.AvgSalesUSD90 /= c90 + } + if c180 > 0 { + stats.AvgSalesUSD180 /= c180 + } + if c365 > 0 { + stats.AvgSalesUSD365 /= c365 + } + if cTotal > 0 { + stats.AvgSalesUSDTotal /= cTotal + } + return stats +} + +func productPerformanceExcelVariantKey(productCode, colorCode, yakaKodu string) string { + return strings.ToUpper(strings.TrimSpace(productCode)) + "|" + strings.ToUpper(strings.TrimSpace(colorCode)) + "|" + strings.ToUpper(strings.TrimSpace(yakaKodu)) +} + +func productPerformanceExcelColorYaka(row models.ProductPerformanceRow) string { + color := strings.TrimSpace(row.ColorCode) + if desc := strings.TrimSpace(row.ColorDescription); color != "" && desc != "" { + color = color + "-" + strings.ToUpper(desc) + } + yaka := strings.TrimSpace(row.YakaKodu) + parts := make([]string, 0, 2) + if color != "" && color != "-" { + parts = append(parts, color) + } + if yaka != "" && yaka != "-" { + parts = append(parts, yaka) + } + return strings.Join(parts, "/") +} + +func productPerformanceExcelMarketName(value string) string { + parts := strings.Split(value, "|") + for i := len(parts) - 1; i >= 0; i-- { + part := strings.TrimSpace(parts[i]) + if part != "" { + return part + } + } + return "" +} + +func productPerformanceExcelBucketLabel(value string) string { + switch strings.TrimSpace(value) { + case "YILDIZ_URUN": + return "Yıldız Ürün" + case "STOKSUZ_TALEP": + return "Stoksuz Talep" + case "STOK_RISKI": + return "Stok Riski" + case "FIYAT_BASKISI": + return "Fiyat Baskısı" + case "FIYAT_FIRSATI": + return "Fiyat Fırsatı" + case "MALIYET_YOK": + return "Maliyet Yok" + case "TAKIP": + return "Takip" + default: + return strings.TrimSpace(value) + } +} + +func productPerformanceExcelCleanOptional(value string) string { + value = strings.TrimSpace(value) + if value == "-" { + return "" + } + return value +} + +func productPerformanceExcelNormalize(value string) string { + return strings.ToUpper(strings.TrimSpace(value)) +} + +func productPerformanceExcelFirstNonEmpty(values ...string) string { + for _, value := range values { + value = strings.TrimSpace(value) + if value != "" { + return value + } + } + return "" +} + +func productPerformanceExcelFirstNonZero(values ...float64) float64 { + for _, value := range values { + if value != 0 && !math.IsNaN(value) && !math.IsInf(value, 0) { + return value + } + } + return 0 +} + +func productPerformanceExcelTotalSalesQty(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow) float64 { + return productPerformanceExcelFirstNonZero(general.SalesQtyTotal, row.SalesQtyTotal) +} + +func productPerformanceExcelTotalSalesUSD(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow) float64 { + return productPerformanceExcelFirstNonZero(general.SalesUSDTotal, row.SalesUSDTotal) +} + +func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 { + if qty == 0 { + return 0 + } + return salesUSD / qty +} + +func productPerformanceExcelStockTurnover(salesQty, stockQty float64) float64 { + if stockQty <= 0 { + return 0 + } + return salesQty / stockQty +} + +func productPerformanceExcelUnitProfit(salesUSD, qty, unitCost float64) float64 { + if qty <= 0 { + return 0 + } + return (salesUSD / qty) - unitCost +} + +func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 { + if qty <= 0 { + return 0 + } + return salesUSD - qty*unitCost +} + +func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 { + if salesUSD == 0 { + return 0 + } + return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD +} + +func productPerformanceExcelRelativeIndex(value, average float64) float64 { + if value <= 0 || average <= 0 { + return 0 + } + return value / average +} + +func productPerformanceExcelProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 { + revenue := productPerformanceExcelRatioScore(salesIndex, 2) + if revenue == 0 { + revenue = productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix)) + } + score := 0.30*productPerformanceExcelMarginScore(margin) + + 0.20*productPerformanceExcelRatioScore(stockTurnover, 1.50) + + 0.20*revenue + + 0.15*productPerformanceExcelRatioScore(marketCount, 8) + + 0.15*productPerformanceExcelRatioScore(customerCount, 25) + return productPerformanceExcelRoundScore(score) +} + +func productPerformanceExcelProductRevenueTarget(suffix string) float64 { + switch suffix { + case "180d": + return 20000 + case "365d": + return 40000 + case "total": + return 120000 + default: + return 10000 + } +} + +func productPerformanceExcelMarginScore(margin float64) float64 { + if margin < 0 { + margin = 0 + } + return productPerformanceExcelRatioScore(margin, 0.40) +} + +func productPerformanceExcelRatioScore(value, target float64) float64 { + if target <= 0 || value <= 0 { + return 0 + } + score := value * 100 / target + if score > 100 { + return 100 + } + if score < 0 { + return 0 + } + return score +} + +func productPerformanceExcelRoundScore(score float64) float64 { + if score < 0 { + score = 0 + } + if score > 100 { + score = 100 + } + return math.Round(score*10000) / 10000 +} + +func productPerformanceExcelFloat(value any) float64 { + switch v := value.(type) { + case float64: + if math.IsNaN(v) || math.IsInf(v, 0) { + return 0 + } + return v + case float32: + return productPerformanceExcelFloat(float64(v)) + case int: + return float64(v) + case int64: + return float64(v) + case int32: + return float64(v) + case uint: + return float64(v) + case uint64: + return float64(v) + case uint32: + return float64(v) + default: + return 0 + } +} + +func productPerformanceExcelColumnWidth(header, kind string) float64 { + if kind == "text" { + if len([]rune(header)) > 14 { + return 24 + } + return 16 + } + if strings.Contains(header, "Marj") || strings.Contains(header, "Skor") || strings.Contains(header, "Endeks") { + return 14 + } + return 16 +} diff --git a/ui/src/pages/OrderPriceList.vue b/ui/src/pages/OrderPriceList.vue index 365f0bc..1f8b28c 100644 --- a/ui/src/pages/OrderPriceList.vue +++ b/ui/src/pages/OrderPriceList.vue @@ -1289,8 +1289,6 @@ function normalizeUploadsPath (storagePath) { function resolveProductImageUrl (item) { if (!item || typeof item !== 'object') return '' - const imageId = Number(item.id || item.ID || 0) - if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content` const thumbUrl = toText(item.thumb_url || item.thumbUrl) if (thumbUrl) return thumbUrl const fullUrl = toText(item.full_url || item.fullUrl) diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index b5339c5..54dc691 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -60,6 +60,7 @@ + @@ -1520,7 +1521,7 @@