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 }