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(*productPerformanceExcelVariant, productPerformanceExcelStats) any } type productPerformanceExcelStats struct { AvgSalesUSD90 float64 AvgSalesUSD180 float64 AvgSalesUSD365 float64 AvgSalesUSDTotal float64 } type productPerformanceExcelVariant struct { KpiDate string ProductCode string ColorCode string ColorDescription string YakaKodu string ItemDescription string Kategori string Seri string YasGrubu string AskiliYan string UrunIlkGrubu string UrunAnaGrubu string UrunAltGrubu string StockQty float64 AvgStock90 float64 AvgStock180 float64 AvgStock365 float64 AvgStockTotal float64 BasePriceUSD float64 CostPriceUSD float64 SalesQty90 float64 SalesQty180 float64 SalesQty365 float64 SalesUSD90 float64 SalesUSD180 float64 SalesUSD365 float64 SalesQtyTotalProduct float64 SalesUSDTotalProduct float64 SalesQtyTotalGeneral float64 SalesUSDTotalGeneral float64 HasGeneralTotal bool MarketCount90 int CustomerCount90 int MarketCountTotal int CustomerCountTotal int Markets90 map[string]bool MarketsTotal map[string]bool MarketFiltered bool PerformanceBucket string Recommendation string LastSaleDate string LastRefNumber string UpdatedAt string } 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 } } productRows, _, 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 } rows := buildProductPerformanceExcelVariants(productRows, generalRows, req.Filters) sortProductPerformanceExcelVariants(rows, req.SortBy, req.Descending == nil || *req.Descending) stats := productPerformanceExcelStatsFor(rows) file, err := buildProductPerformanceExcelFile(rows, 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_renk_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 buildProductPerformanceExcelVariants(productRows []models.ProductPerformanceRow, generalRows []models.ProductPerformanceGeneralRow, filters map[string][]string) []*productPerformanceExcelVariant { byKey := map[string]*productPerformanceExcelVariant{} marketFiltered := productPerformanceExcelHasFilter(filters, "market_key") for _, row := range productRows { if !productPerformanceExcelProductRowMatchesFilters(row, filters) { continue } key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu) v := byKey[key] if v == nil { v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered} byKey[key] = v } v.addProductRow(row) } for _, row := range generalRows { if !productPerformanceExcelGeneralRowMatchesFilters(row, filters) { continue } key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu) v := byKey[key] if v == nil { v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered} byKey[key] = v } v.addGeneralRow(row) } out := make([]*productPerformanceExcelVariant, 0, len(byKey)) for _, row := range byKey { if productPerformanceExcelVariantMatchesPostFilters(row, filters) { out = append(out, row) } } return out } func (v *productPerformanceExcelVariant) addProductRow(row models.ProductPerformanceRow) { v.fillDimensions( row.KpiDate, row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription, row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu, row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, row.UpdatedAt, ) v.SalesQty90 += row.SalesQty90 v.SalesQty180 += row.SalesQty180 v.SalesQty365 += row.SalesQty365 v.SalesUSD90 += row.SalesUSD90 v.SalesUSD180 += row.SalesUSD180 v.SalesUSD365 += row.SalesUSD365 v.AvgStock90 = productPerformanceExcelFirstNonZero(v.AvgStock90, row.AvgStock90) v.AvgStock180 = productPerformanceExcelFirstNonZero(v.AvgStock180, row.AvgStock180) v.AvgStock365 = productPerformanceExcelFirstNonZero(v.AvgStock365, row.AvgStock365) v.AvgStockTotal = productPerformanceExcelFirstNonZero(v.AvgStockTotal, row.AvgStockTotal) v.SalesQtyTotalProduct += row.SalesQtyTotal v.SalesUSDTotalProduct += row.SalesUSDTotal v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90) if row.SalesUSD90 > 0 { v.CustomerCount90 += row.CustomerCount90 } v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket) v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation) v.addMarket(row.MarketKey, row.SalesQty90, row.SalesUSD90, row.SalesQtyTotal, row.SalesUSDTotal) } func (v *productPerformanceExcelVariant) addGeneralRow(row models.ProductPerformanceGeneralRow) { v.fillDimensions( productPerformanceExcelFirstNonEmpty(row.PeriodEnd, row.PeriodStart), row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription, row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu, row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, "", ) v.SalesQtyTotalGeneral += row.SalesQtyTotal v.SalesUSDTotalGeneral += row.SalesUSDTotal v.HasGeneralTotal = true v.MarketCountTotal = productPerformanceExcelMaxInt(v.MarketCountTotal, row.MarketCountTotal) if row.SalesUSDTotal > 0 { v.CustomerCountTotal += row.CustomerCountTotal } v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket) v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation) v.addMarket(row.MarketKey, 0, 0, row.SalesQtyTotal, row.SalesUSDTotal) } func (v *productPerformanceExcelVariant) fillDimensions(kpiDate, productCode, colorCode, colorDescription, yakaKodu, itemDescription, kategori, seri, yasGrubu, askiliYan, urunIlkGrubu, urunAnaGrubu, urunAltGrubu string, basePrice, costPrice, stockQty float64, lastSaleDate, lastRefNumber, updatedAt string) { v.KpiDate = productPerformanceExcelFirstNonEmpty(v.KpiDate, kpiDate) v.ProductCode = productPerformanceExcelFirstNonEmpty(v.ProductCode, productCode) v.ColorCode = productPerformanceExcelFirstNonEmpty(v.ColorCode, colorCode) v.ColorDescription = productPerformanceExcelFirstNonEmpty(v.ColorDescription, colorDescription) v.YakaKodu = productPerformanceExcelFirstNonEmpty(v.YakaKodu, yakaKodu) v.ItemDescription = productPerformanceExcelFirstNonEmpty(v.ItemDescription, itemDescription) v.Kategori = productPerformanceExcelFirstNonEmpty(v.Kategori, kategori) v.Seri = productPerformanceExcelFirstNonEmpty(v.Seri, seri) v.YasGrubu = productPerformanceExcelFirstNonEmpty(v.YasGrubu, yasGrubu) v.AskiliYan = productPerformanceExcelFirstNonEmpty(v.AskiliYan, productPerformanceExcelCleanOptional(askiliYan)) v.UrunIlkGrubu = productPerformanceExcelFirstNonEmpty(v.UrunIlkGrubu, productPerformanceExcelCleanOptional(urunIlkGrubu)) v.UrunAnaGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAnaGrubu, urunAnaGrubu) v.UrunAltGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAltGrubu, urunAltGrubu) v.BasePriceUSD = productPerformanceExcelFirstNonZero(v.BasePriceUSD, basePrice) v.CostPriceUSD = productPerformanceExcelFirstNonZero(v.CostPriceUSD, costPrice) v.CostPriceUSD, v.BasePriceUSD = productPerformanceExcelNormalizeCostPair(v.CostPriceUSD, v.BasePriceUSD) if stockQty > v.StockQty { v.StockQty = stockQty } if lastSaleDate > v.LastSaleDate { v.LastSaleDate = lastSaleDate v.LastRefNumber = lastRefNumber } if updatedAt > v.UpdatedAt { v.UpdatedAt = updatedAt } } func (v *productPerformanceExcelVariant) addMarket(marketKey string, salesQty90, salesUSD90, salesQtyTotal, salesUSDTotal float64) { market := productPerformanceExcelMarketName(marketKey) if market == "" || market == "STOK" { return } if salesUSD90 > 0 { v.Markets90[market] = true } if salesUSDTotal > 0 { v.MarketsTotal[market] = true } } func (v *productPerformanceExcelVariant) totalQty() float64 { if v.HasGeneralTotal { return v.SalesQtyTotalGeneral } return v.SalesQtyTotalProduct } func (v *productPerformanceExcelVariant) totalUSD() float64 { if v.HasGeneralTotal { return v.SalesUSDTotalGeneral } return v.SalesUSDTotalProduct } func (v *productPerformanceExcelVariant) marketCount90() int { if v.MarketFiltered { return len(v.Markets90) } return productPerformanceExcelMaxInt(v.MarketCount90, len(v.Markets90)) } func (v *productPerformanceExcelVariant) marketCountTotal() int { if v.MarketFiltered { return len(v.MarketsTotal) } return productPerformanceExcelMaxInt(v.MarketCountTotal, len(v.MarketsTotal)) } func (v *productPerformanceExcelVariant) marketsLabel() string { return productPerformanceExcelSortedSetLabel(v.MarketsTotal) } func buildProductPerformanceExcelFile(rows []*productPerformanceExcelVariant, 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 for colIndex, col := range columns { cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow) value := col.Value(row, stats) if col.Kind == "text" { _ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value))) } else { _ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value)) } } } lastRow := productPerformanceExcelMaxInt(len(rows)+1, 2) if len(columns) > 0 { lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), lastRow) _ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{}) } for i, col := range columns { colName, _ := excelize.ColumnNumberToName(i + 1) _ = f.SetColWidth(sheet, colName, colName, productPerformanceExcelColumnWidth(col.Header, col.Kind)) 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, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft"}) return f, nil } func productPerformanceExcelColumns() []productPerformanceExcelColumn { base := []productPerformanceExcelColumn{ textExcelColumn("KPI Tarihi", func(v *productPerformanceExcelVariant) any { return v.KpiDate }), textExcelColumn("Ürün İlk Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunIlkGrubu }), textExcelColumn("Askılı/Yan", func(v *productPerformanceExcelVariant) any { return v.AskiliYan }), textExcelColumn("Kategori", func(v *productPerformanceExcelVariant) any { return v.Kategori }), textExcelColumn("Ürün Ana Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAnaGrubu }), textExcelColumn("Ürün Alt Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAltGrubu }), textExcelColumn("Ürün", func(v *productPerformanceExcelVariant) any { return v.ProductCode }), textExcelColumn("Açıklama", func(v *productPerformanceExcelVariant) any { return v.ItemDescription }), textExcelColumn("Renk", func(v *productPerformanceExcelVariant) any { return v.ColorCode }), textExcelColumn("Renk Açıklama", func(v *productPerformanceExcelVariant) any { return v.ColorDescription }), textExcelColumn("Yaka", func(v *productPerformanceExcelVariant) any { return v.YakaKodu }), textExcelColumn("Renk/Yaka", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelColorYaka(v.ColorCode, v.ColorDescription, v.YakaKodu) }), textExcelColumn("Piyasalar", func(v *productPerformanceExcelVariant) any { return v.marketsLabel() }), numberExcelColumn("Toplam Stok", "int", func(v *productPerformanceExcelVariant) any { return v.StockQty }), } base = append(base, productPerformanceExcelPeriodColumns("90G", "90d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { return v.SalesQty90, v.SalesUSD90, v.marketCount90(), v.CustomerCount90 })...) base = append(base, productPerformanceExcelPeriodColumns("180G", "180d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { return v.SalesQty180, v.SalesUSD180, v.marketCount90(), v.CustomerCount90 })...) base = append(base, productPerformanceExcelPeriodColumns("360G", "365d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { return v.SalesQty365, v.SalesUSD365, v.marketCount90(), v.CustomerCount90 })...) base = append(base, productPerformanceExcelPeriodColumns("Genel", "total", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { return v.totalQty(), v.totalUSD(), v.marketCountTotal(), v.CustomerCountTotal })...) base = append(base, textExcelColumn("Durum", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelStatus(v) }), textExcelColumn("Öneri", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelRecommendation(v) }), textExcelColumn("Son Satış", func(v *productPerformanceExcelVariant) any { return v.LastSaleDate }), textExcelColumn("Son Ref", func(v *productPerformanceExcelVariant) any { return v.LastRefNumber }), textExcelColumn("Güncelleme", func(v *productPerformanceExcelVariant) any { return v.UpdatedAt }), ) return base } func productPerformanceExcelPeriodColumns(label, suffix string, values func(*productPerformanceExcelVariant) (float64, float64, int, int)) []productPerformanceExcelColumn { return []productPerformanceExcelColumn{ numberExcelColumn(label+" Toplam Adet", "int", func(v *productPerformanceExcelVariant) any { qty, _, _, _ := values(v) return qty }), numberExcelColumn(label+" Toplam Ciro USD", "number", func(v *productPerformanceExcelVariant) any { _, usd, _, _ := values(v) return usd }), numberExcelColumn(label+" Ort. Satış Fiyatı USD", "number", func(v *productPerformanceExcelVariant) any { qty, usd, _, _ := values(v) return productPerformanceExcelAvgPrice(usd, qty) }), numberExcelColumn(label+" Yıllık Stok Devir", "number", func(v *productPerformanceExcelVariant) any { qty, _, _, _ := values(v) return productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix)) }), numberExcelColumn(label+" Taban Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.BasePriceUSD }), numberExcelColumn(label+" Çıplak Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.CostPriceUSD }), numberExcelColumn(label+" Taban Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any { qty, usd, _, _ := values(v) return productPerformanceExcelGrossProfit(usd, qty, v.BasePriceUSD) }), numberExcelColumn(label+" Çıplak Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any { qty, usd, _, _ := values(v) return productPerformanceExcelGrossProfit(usd, qty, v.CostPriceUSD) }), numberExcelColumn(label+" Taban Marj", "percent", func(v *productPerformanceExcelVariant) any { qty, usd, _, _ := values(v) return productPerformanceExcelMargin(usd, qty, v.BasePriceUSD) }), numberExcelColumn(label+" Çıplak Marj", "percent", func(v *productPerformanceExcelVariant) any { qty, usd, _, _ := values(v) return productPerformanceExcelMargin(usd, qty, v.CostPriceUSD) }), numberExcelColumn(label+" Tekil Piyasa", "int", func(v *productPerformanceExcelVariant) any { _, _, markets, _ := values(v) return markets }), numberExcelColumn(label+" Tekil Müşteri", "int", func(v *productPerformanceExcelVariant) any { _, _, _, customers := values(v) return customers }), numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any { qty, usd, markets, customers := values(v) days := productPerformanceExcelPeriodDays(v, suffix) return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), days), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD), days) }), } } func textExcelColumn(header string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn { return productPerformanceExcelColumn{Header: header, Kind: "text", Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }} } func numberExcelColumn(header, kind string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn { return productPerformanceExcelColumn{Header: header, Kind: kind, Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }} } func productPerformanceExcelProductRowMatchesFilters(row models.ProductPerformanceRow, filters map[string][]string) bool { for field, selected := range productPerformanceExcelFilterSets(filters) { if field == "performance_bucket" { continue } if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelProductFilterCandidates(row, field)) { return false } } return true } func productPerformanceExcelGeneralRowMatchesFilters(row models.ProductPerformanceGeneralRow, filters map[string][]string) bool { for field, selected := range productPerformanceExcelFilterSets(filters) { if field == "performance_bucket" { continue } if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelGeneralFilterCandidates(row, field)) { return false } } return true } func productPerformanceExcelVariantMatchesPostFilters(row *productPerformanceExcelVariant, filters map[string][]string) bool { selected := productPerformanceExcelFilterSets(filters)["performance_bucket"] if len(selected) == 0 { return true } return productPerformanceExcelMatchesAny(selected, []string{row.PerformanceBucket, productPerformanceExcelStatus(row)}) } func productPerformanceExcelFilterSets(filters map[string][]string) map[string]map[string]bool { out := map[string]map[string]bool{} for field, values := range filters { field = strings.TrimSpace(field) for _, value := range values { value = productPerformanceExcelNormalize(value) if value == "" { continue } if out[field] == nil { out[field] = map[string]bool{} } out[field][value] = true } } return out } func productPerformanceExcelHasFilter(filters map[string][]string, field string) bool { for _, value := range filters[strings.TrimSpace(field)] { if strings.TrimSpace(value) != "" { return true } } return false } func productPerformanceExcelProductFilterCandidates(row models.ProductPerformanceRow, 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.ColorCode, row.ColorDescription, row.YakaKodu), 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{row.UrunAnaGrubu} case "urun_alt_grubu": return []string{row.UrunAltGrubu} case "market_key": return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)} default: return nil } } func productPerformanceExcelGeneralFilterCandidates(row 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.ColorCode, row.ColorDescription, row.YakaKodu), 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{row.UrunAnaGrubu} case "urun_alt_grubu": return []string{row.UrunAltGrubu} case "market_key": return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)} default: return nil } } func productPerformanceExcelMatchesAny(selected map[string]bool, candidates []string) bool { if len(selected) == 0 { return true } for _, candidate := range candidates { if selected[productPerformanceExcelNormalize(candidate)] { return true } } return false } func sortProductPerformanceExcelVariants(rows []*productPerformanceExcelVariant, 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 { cmp := productPerformanceExcelCompare(rows[i], rows[j], sortBy) if cmp == 0 { cmp = strings.Compare(productPerformanceExcelHierarchySortKey(rows[i]), productPerformanceExcelHierarchySortKey(rows[j])) } if desc { return cmp > 0 } return cmp < 0 }) } func productPerformanceExcelCompare(a, b *productPerformanceExcelVariant, sortBy string) int { if productPerformanceExcelIsNumericSort(sortBy) { av := productPerformanceExcelSortNumber(a, sortBy) bv := productPerformanceExcelSortNumber(b, sortBy) if av < bv { return -1 } if av > bv { return 1 } return 0 } return strings.Compare(productPerformanceExcelNormalize(productPerformanceExcelSortText(a, sortBy)), productPerformanceExcelNormalize(productPerformanceExcelSortText(b, sortBy))) } 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_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", "performance_score", "performance_score_90d", "performance_score_total": return true default: return false } } func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, 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 row.totalQty() 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 row.totalUSD() 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 productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty()) case "stock_turnover_90d": return productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d")) case "stock_turnover_180d": return productPerformanceExcelStockTurnover(row.SalesQty180, productPerformanceExcelAvgStock(row, "180d"), productPerformanceExcelPeriodDays(row, "180d")) case "stock_turnover_365d": return productPerformanceExcelStockTurnover(row.SalesQty365, productPerformanceExcelAvgStock(row, "365d"), productPerformanceExcelPeriodDays(row, "365d")) case "stock_turnover_total": return productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")) case "base_price_usd": return row.BasePriceUSD case "cost_price_usd": return row.CostPriceUSD case "market_count_90d": return float64(row.marketCount90()) case "customer_count_90d": return float64(row.CustomerCount90) case "market_count_total": return float64(row.marketCountTotal()) case "customer_count_total": return float64(row.CustomerCountTotal) case "performance_score", "performance_score_90d": days := productPerformanceExcelPeriodDays(row, "90d") return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), days), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD), days) case "performance_score_total": days := productPerformanceExcelPeriodDays(row, "total") return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), days), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD), days) default: return 0 } } func productPerformanceExcelSortText(row *productPerformanceExcelVariant, sortBy string) string { switch sortBy { case "product_code": return row.ProductCode case "color_yaka": return productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu) 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 row.AskiliYan case "urun_ilk_grubu": return row.UrunIlkGrubu case "urun_ana_grubu": return row.UrunAnaGrubu case "urun_alt_grubu": return row.UrunAltGrubu case "market_key": return row.marketsLabel() case "performance_bucket": return productPerformanceExcelStatus(row) default: return productPerformanceExcelHierarchySortKey(row) } } func productPerformanceExcelStatsFor(rows []*productPerformanceExcelVariant) productPerformanceExcelStats { var stats productPerformanceExcelStats var c90, c180, c365, cTotal float64 for _, row := range rows { 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 row.totalUSD() > 0 { stats.AvgSalesUSDTotal += row.totalUSD() 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 productPerformanceExcelHierarchySortKey(row *productPerformanceExcelVariant) string { return strings.Join([]string{row.UrunIlkGrubu, row.AskiliYan, row.Kategori, row.UrunAnaGrubu, row.UrunAltGrubu, row.ProductCode, row.ColorCode, row.YakaKodu}, "|") } func productPerformanceExcelColorYaka(colorCode, colorDescription, yakaKodu string) string { color := strings.TrimSpace(colorCode) if desc := strings.TrimSpace(colorDescription); color != "" && desc != "" { color = color + "-" + strings.ToUpper(desc) } yaka := strings.TrimSpace(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 productPerformanceExcelStatus(row *productPerformanceExcelVariant) string { switch { case row.totalQty() > 0 && row.totalUSD() <= 0: return "Ciro/Fiyat Kontrol" case row.totalQty() <= 0 && row.StockQty > 0: return "Stok Riski" case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0: return "Fiyat Baskısı" case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD), productPerformanceExcelPeriodDays(row, "total")) >= 70: return "Yıldız Ürün" default: return productPerformanceExcelBucketLabel(row.PerformanceBucket) } } func productPerformanceExcelRecommendation(row *productPerformanceExcelVariant) string { switch { case row.totalQty() > 0 && row.totalUSD() <= 0: return "Satış adedi var ama USD ciro/fiyat yok. Satış tutarı aktarımı ve döviz dönüşümü kontrol edilmeli." case row.totalQty() <= 0 && row.StockQty > 0: return "Satış yok, stok duruyor. Piyasa/fiyat aksiyonu gerekli." case row.Recommendation != "": return row.Recommendation default: return "Düzenli takip." } } 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 productPerformanceExcelNormalizeCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) { if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD { return basePriceUSD, costPriceUSD } return costPriceUSD, basePriceUSD } func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 { if qty <= 0 { return 0 } return salesUSD / qty } const productPerformanceExcelStockTurnoverYearDays = 360.0 func productPerformanceExcelAvgStock(row *productPerformanceExcelVariant, suffix string) float64 { switch suffix { case "90d": return productPerformanceExcelFirstNonZero(row.AvgStock90, row.StockQty) case "180d": return productPerformanceExcelFirstNonZero(row.AvgStock180, row.StockQty) case "365d": return productPerformanceExcelFirstNonZero(row.AvgStock365, row.StockQty) case "total": return productPerformanceExcelFirstNonZero(row.AvgStockTotal, row.StockQty) default: return row.StockQty } } func productPerformanceExcelPeriodDays(row *productPerformanceExcelVariant, suffix string) float64 { switch suffix { case "90d": return 90 case "180d": return 180 case "365d": return 360 case "total": end, err := time.Parse("2006-01-02", strings.TrimSpace(row.KpiDate)) if err != nil { return 0 } start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) if end.Before(start) { return 0 } return end.Sub(start).Hours()/24 + 1 default: return 0 } } func productPerformanceExcelStockTurnover(salesQty, avgStock, periodDays float64) float64 { if salesQty <= 0 || avgStock <= 0 { return 0 } raw := salesQty / avgStock if periodDays <= 0 { return raw } return raw * productPerformanceExcelStockTurnoverYearDays / periodDays } func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 { 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 productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64, periodDays ...float64) float64 { if salesUSD <= 0 { return 1 } days := productPerformanceExcelScorePeriodDays(suffix, periodDays...) revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTargetForDays(suffix, days)) score := 0.30*productPerformanceExcelMarginScore(margin) + 0.20*productPerformanceExcelRatioScore(stockTurnover, 4) + 0.20*revenueScore + 0.05*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) + 0.25*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTargetForDays(suffix, days)) return productPerformanceExcelRoundScore(score) } func productPerformanceExcelMarketSpreadTarget(suffix string) float64 { switch suffix { case "90d": return 3 case "180d": return 3 case "365d": return 6 default: return 6 } } func productPerformanceExcelCustomerSpreadTarget(suffix string) float64 { return productPerformanceExcelCustomerSpreadTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix)) } func productPerformanceExcelCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 { switch suffix { case "90d": return 8 case "180d": return 20 case "365d": return 50 default: return 20 * productPerformanceExcelTotalPeriodMultiplier(periodDays) } } func productPerformanceExcelProductRevenueTarget(suffix string) float64 { return productPerformanceExcelProductRevenueTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix)) } func productPerformanceExcelProductRevenueTargetForDays(suffix string, periodDays float64) float64 { switch suffix { case "180d": return 50000 case "365d": return 100000 case "total": return 50000 * productPerformanceExcelTotalPeriodMultiplier(periodDays) default: return 25000 } } func productPerformanceExcelScorePeriodDays(suffix string, periodDays ...float64) float64 { if len(periodDays) > 0 && periodDays[0] > 0 { return periodDays[0] } switch suffix { case "90d": return 90 case "180d": return 180 case "365d": return 360 case "total": start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) now := time.Now() if now.Before(start) { return 0 } return now.Sub(start).Hours()/24 + 1 default: return 0 } } func productPerformanceExcelTotalPeriodMultiplier(periodDays float64) float64 { if periodDays <= 0 { periodDays = 180 } return math.Max(1, periodDays/180.0) } func productPerformanceExcelMarginScore(margin float64) float64 { if margin < 0 { margin = 0 } return productPerformanceExcelRatioScore(margin, 0.45) } 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 productPerformanceExcelSortedSetLabel(values map[string]bool) string { out := make([]string, 0, len(values)) for value := range values { if strings.TrimSpace(value) != "" { out = append(out, value) } } sort.Strings(out) return strings.Join(out, ", ") } func productPerformanceExcelColumnWidth(header, kind string) float64 { if kind == "text" { if strings.Contains(header, "Piyasalar") || strings.Contains(header, "Öneri") { return 34 } if len([]rune(header)) > 14 { return 24 } return 16 } if strings.Contains(header, "Marj") || strings.Contains(header, "Skor") { return 14 } return 16 } func productPerformanceExcelMaxInt(a, b int) int { if a > b { return a } return b }