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