1099 lines
38 KiB
Go
1099 lines
38 KiB
Go
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)
|
||
return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix)), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD))
|
||
}),
|
||
}
|
||
}
|
||
|
||
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":
|
||
return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d")), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD))
|
||
case "performance_score_total":
|
||
return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD))
|
||
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)) >= 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) float64 {
|
||
if salesUSD <= 0 {
|
||
return 1
|
||
}
|
||
revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix))
|
||
score := 0.30*productPerformanceExcelMarginScore(margin) +
|
||
0.20*productPerformanceExcelRatioScore(stockTurnover, 4) +
|
||
0.20*revenueScore +
|
||
0.15*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) +
|
||
0.15*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTarget(suffix))
|
||
return productPerformanceExcelRoundScore(score)
|
||
}
|
||
|
||
func productPerformanceExcelMarketSpreadTarget(suffix string) float64 {
|
||
switch suffix {
|
||
case "90d":
|
||
return 3
|
||
case "180d":
|
||
return 5
|
||
case "365d":
|
||
return 6
|
||
default:
|
||
return 8
|
||
}
|
||
}
|
||
|
||
func productPerformanceExcelCustomerSpreadTarget(suffix string) float64 {
|
||
switch suffix {
|
||
case "90d":
|
||
return 8
|
||
case "180d":
|
||
return 15
|
||
case "365d":
|
||
return 18
|
||
default:
|
||
return 25
|
||
}
|
||
}
|
||
|
||
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 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
|
||
}
|