Fix product performance grouped JSON build
This commit is contained in:
@@ -939,6 +939,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
wrapV3(routes.GetProductPerformanceHandler(pgDB)),
|
wrapV3(routes.GetProductPerformanceHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/product-performance/export-excel", "POST",
|
||||||
|
"pricing", "view",
|
||||||
|
wrapV3(routes.ExportProductPerformanceExcelHandler(pgDB)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/product-performance/summary", "GET",
|
"/api/pricing/product-performance/summary", "GET",
|
||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
|
|||||||
@@ -3937,8 +3937,8 @@ func ListProductPerformanceGroupedFilterOptions(ctx context.Context, pg *sql.DB,
|
|||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if req.Limit <= 0 || req.Limit > 5000 {
|
if req.Limit <= 0 || req.Limit > 50000 {
|
||||||
req.Limit = 5000
|
req.Limit = 50000
|
||||||
}
|
}
|
||||||
levels := sanitizeProductPerformanceGroupLevels(req.GroupLevels)
|
levels := sanitizeProductPerformanceGroupLevels(req.GroupLevels)
|
||||||
if len(levels) == 0 {
|
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_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(sales_qty_180d) > 0 THEN SUM(sales_usd_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS avg_price_usd_180d,
|
||||||
CASE
|
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
|
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 WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END)
|
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 WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
|
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0)
|
||||||
ELSE 0
|
ELSE COALESCE(AVG(NULLIF(cost_price_usd,0)),0)
|
||||||
END AS cost_price_usd,
|
END AS cost_price_usd,
|
||||||
CASE
|
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
|
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 WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END)
|
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 WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
|
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0)
|
||||||
ELSE 0
|
ELSE COALESCE(AVG(NULLIF(base_price_usd,0)),0)
|
||||||
END AS base_price_usd,
|
END AS base_price_usd,
|
||||||
CASE
|
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
|
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 WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END)
|
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 WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
|
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0)
|
||||||
ELSE 0
|
ELSE COALESCE(AVG(NULLIF(base_price_try,0)),0)
|
||||||
END AS base_price_try,
|
END AS base_price_try,
|
||||||
COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
|
COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
|
||||||
COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d,
|
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
|
n.Avg[key] = state
|
||||||
}
|
}
|
||||||
number := floatFromAny(value)
|
number := floatFromAny(value)
|
||||||
weight := floatFromMap(row, productPerformanceMetricWeightField(key))
|
weight := productPerformanceMetricWeight(row, key)
|
||||||
if weight > 0 {
|
if weight > 0 {
|
||||||
state.Weighted += number * weight
|
state.Weighted += number * weight
|
||||||
state.Weight += 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 {
|
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
|
||||||
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
|
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
|
||||||
}
|
}
|
||||||
|
if _, ok := out["customer_score_90d"]; !ok {
|
||||||
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
|
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
|
||||||
|
}
|
||||||
|
if _, ok := out["customer_score_180d"]; !ok {
|
||||||
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
|
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
|
||||||
|
}
|
||||||
|
if _, ok := out["customer_score_365d"]; !ok {
|
||||||
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
|
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
|
||||||
|
}
|
||||||
|
if _, ok := out["customer_score_total"]; !ok {
|
||||||
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
|
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
|
||||||
|
}
|
||||||
|
if _, ok := out["performance_score_90d"]; !ok {
|
||||||
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
|
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
|
||||||
|
}
|
||||||
|
if _, ok := out["performance_score_180d"]; !ok {
|
||||||
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
|
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
|
||||||
|
}
|
||||||
|
if _, ok := out["performance_score_365d"]; !ok {
|
||||||
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
|
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
|
||||||
|
}
|
||||||
|
if _, ok := out["performance_score_total"]; !ok {
|
||||||
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
|
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
|
||||||
|
}
|
||||||
|
if _, ok := out["performance_score"]; !ok {
|
||||||
out["performance_score"] = productPerformanceGroupScore(out, groupField)
|
out["performance_score"] = productPerformanceGroupScore(out, groupField)
|
||||||
|
}
|
||||||
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
||||||
score := productPerformanceOrderGroupScore(out)
|
score := productPerformanceOrderGroupScore(out)
|
||||||
out["performance_score_90d"] = score
|
out["performance_score_90d"] = score
|
||||||
@@ -6087,6 +6105,9 @@ func shouldAverageProductPerformanceField(field string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceMetricWeightField(field string) string {
|
func productPerformanceMetricWeightField(field string) string {
|
||||||
|
if productPerformanceUsesCostWeight(field) {
|
||||||
|
return "__product_cost_weight"
|
||||||
|
}
|
||||||
if strings.Contains(field, "_180d") {
|
if strings.Contains(field, "_180d") {
|
||||||
return "sales_qty_180d"
|
return "sales_qty_180d"
|
||||||
}
|
}
|
||||||
@@ -6105,11 +6126,44 @@ func productPerformanceMetricWeightField(field string) string {
|
|||||||
return "sales_qty_90d"
|
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 {
|
func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qtyField string) float64 {
|
||||||
var weighted, qty float64
|
var weighted, qty float64
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
value := floatFromMap(row, valueField)
|
value := floatFromMap(row, valueField)
|
||||||
weight := floatFromMap(row, qtyField)
|
weight := floatFromMap(row, qtyField)
|
||||||
|
if qtyField == "__product_cost_weight" {
|
||||||
|
weight = productPerformanceCostWeight(row)
|
||||||
|
}
|
||||||
if weight > 0 {
|
if weight > 0 {
|
||||||
weighted += value * weight
|
weighted += value * weight
|
||||||
qty += weight
|
qty += weight
|
||||||
@@ -6118,9 +6172,50 @@ func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qt
|
|||||||
if qty > 0 {
|
if qty > 0 {
|
||||||
return weighted / qty
|
return weighted / qty
|
||||||
}
|
}
|
||||||
|
if qtyField == "__product_cost_weight" {
|
||||||
|
return averageProductPerformanceDistinctVariantField(rows, valueField)
|
||||||
|
}
|
||||||
return 0
|
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 {
|
func dominantProductPerformanceValue(rows []map[string]any, field string) string {
|
||||||
counts := map[string]int{}
|
counts := map[string]int{}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type ProductImageItem struct {
|
|||||||
ThumbURL string `json:"thumb_url,omitempty"`
|
ThumbURL string `json:"thumb_url,omitempty"`
|
||||||
FullURL string `json:"full_url,omitempty"`
|
FullURL string `json:"full_url,omitempty"`
|
||||||
StoredInDB bool `json:"stored_in_db,omitempty"`
|
StoredInDB bool `json:"stored_in_db,omitempty"`
|
||||||
|
HasDBContent bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductImageBatchItem struct {
|
type ProductImageBatchItem struct {
|
||||||
@@ -141,14 +142,68 @@ func enrichProductImageItem(it *ProductImageItem) {
|
|||||||
if it == nil {
|
if it == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if it.ID > 0 {
|
|
||||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
|
||||||
}
|
|
||||||
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
|
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
|
||||||
it.UUID = u
|
it.UUID = u
|
||||||
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
|
it.ThumbURL = productImagePublicURLIfExists("uploads/image/t300/" + u + ".jpg")
|
||||||
it.FullURL = "/uploads/image/" + 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
|
// POST /api/product-images/batch
|
||||||
@@ -224,6 +279,7 @@ ranked AS (
|
|||||||
COALESCE(b.file_size,0) AS file_size,
|
COALESCE(b.file_size,0) AS file_size,
|
||||||
COALESCE(b.storage_path,'') AS storage_path,
|
COALESCE(b.storage_path,'') AS storage_path,
|
||||||
COALESCE(b.stored_in_db,false) AS stored_in_db,
|
COALESCE(b.stored_in_db,false) AS stored_in_db,
|
||||||
|
(COALESCE(octet_length(b.bin),0) > 0) AS has_db_content,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY mi.key
|
PARTITION BY mi.key
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -241,9 +297,9 @@ ranked AS (
|
|||||||
AND b.typ='img'
|
AND b.typ='img'
|
||||||
AND b.src_id=mi.mmitem_id
|
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
|
FROM ranked
|
||||||
WHERE rn <= 1
|
WHERE rn <= 5
|
||||||
ORDER BY key, rn
|
ORDER BY key, rn
|
||||||
`, string(payload))
|
`, string(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -257,10 +313,13 @@ ORDER BY key, rn
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var key string
|
var key string
|
||||||
var it ProductImageItem
|
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
|
continue
|
||||||
}
|
}
|
||||||
enrichProductImageItem(&it)
|
enrichProductImageItem(&it)
|
||||||
|
if !productImageItemHasURL(&it) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
grouped[key] = append(grouped[key], it)
|
grouped[key] = append(grouped[key], it)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
@@ -351,7 +410,8 @@ SELECT
|
|||||||
COALESCE(file_name,'') AS file_name,
|
COALESCE(file_name,'') AS file_name,
|
||||||
COALESCE(file_size,0) AS file_size,
|
COALESCE(file_size,0) AS file_size,
|
||||||
COALESCE(storage_path,'') AS storage_path,
|
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
|
FROM dfblob
|
||||||
WHERE typ='img'
|
WHERE typ='img'
|
||||||
AND src_table='mmitem'
|
AND src_table='mmitem'
|
||||||
@@ -382,12 +442,14 @@ ORDER BY
|
|||||||
items := make([]ProductImageItem, 0, 16)
|
items := make([]ProductImageItem, 0, 16)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var it ProductImageItem
|
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
|
continue
|
||||||
}
|
}
|
||||||
enrichProductImageItem(&it)
|
enrichProductImageItem(&it)
|
||||||
|
if productImageItemHasURL(&it) {
|
||||||
items = append(items, it)
|
items = append(items, it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ func GetProductPerformanceGroupedFilterOptionsHandler(pg *sql.DB) http.HandlerFu
|
|||||||
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
|
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
|
||||||
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
||||||
fields := splitCSVQuery(r.URL.Query().Get("fields"))
|
fields := splitCSVQuery(r.URL.Query().Get("fields"))
|
||||||
limit := intQuery(r, "limit", 5000)
|
limit := intQuery(r, "limit", 50000)
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
var body struct {
|
var body struct {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1289,8 +1289,6 @@ function normalizeUploadsPath (storagePath) {
|
|||||||
|
|
||||||
function resolveProductImageUrl (item) {
|
function resolveProductImageUrl (item) {
|
||||||
if (!item || typeof item !== 'object') return ''
|
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)
|
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
|
||||||
if (thumbUrl) return thumbUrl
|
if (thumbUrl) return thumbUrl
|
||||||
const fullUrl = toText(item.full_url || item.fullUrl)
|
const fullUrl = toText(item.full_url || item.fullUrl)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
|
<q-btn dense outline color="positive" icon="grid_on" label="Excel" :loading="excelExportLoading" :disable="pageBusy || excelExportLoading" @click="exportProductPerformanceExcel" />
|
||||||
<q-btn dense outline color="secondary" icon="refresh" label="Yenile" :loading="loading" :disable="pageBusy" @click="reload" />
|
<q-btn dense outline color="secondary" icon="refresh" label="Yenile" :loading="loading" :disable="pageBusy" @click="reload" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1520,7 +1521,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { Notify } from 'quasar'
|
import { Notify } from 'quasar'
|
||||||
import api from 'src/services/api'
|
import api, { extractApiErrorDetail } from 'src/services/api'
|
||||||
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
||||||
|
|
||||||
const performanceStore = useProductPerformanceStore()
|
const performanceStore = useProductPerformanceStore()
|
||||||
@@ -1544,6 +1545,7 @@ function formatPercent (value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const excelExportLoading = ref(false)
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const detailMainGroupOptionRows = ref([])
|
const detailMainGroupOptionRows = ref([])
|
||||||
const generalRows = ref([])
|
const generalRows = ref([])
|
||||||
@@ -1658,6 +1660,18 @@ const backendGroupedFilterFields = new Set([
|
|||||||
'customer_name',
|
'customer_name',
|
||||||
'performance_bucket'
|
'performance_bucket'
|
||||||
])
|
])
|
||||||
|
const productPerformanceExcelExportFilterFields = new Set([
|
||||||
|
'kategori',
|
||||||
|
'askili_yan',
|
||||||
|
'urun_ilk_grubu',
|
||||||
|
'urun_ana_grubu',
|
||||||
|
'urun_alt_grubu',
|
||||||
|
'product_code',
|
||||||
|
'color_yaka',
|
||||||
|
'color_code',
|
||||||
|
'yaka_kodu',
|
||||||
|
'performance_bucket'
|
||||||
|
])
|
||||||
const performanceTabs = [
|
const performanceTabs = [
|
||||||
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
||||||
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
|
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
|
||||||
@@ -2312,6 +2326,105 @@ const customerPeriodScoreColumns = [
|
|||||||
{ name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), align: 'right', sortable: true }
|
{ name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), align: 'right', sortable: true }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const metricLabelOverrides = {
|
||||||
|
stock_qty: 'Toplam Stok',
|
||||||
|
sales_qty: 'Toplam Adet',
|
||||||
|
sales_qty_90d: '90G Toplam Adet',
|
||||||
|
sales_qty_180d: '180G Toplam Adet',
|
||||||
|
sales_qty_365d: '360G Toplam Adet',
|
||||||
|
sales_qty_total: 'Genel Toplam Adet',
|
||||||
|
sales_usd: 'Toplam Ciro USD',
|
||||||
|
sales_usd_90d: '90G Toplam Ciro USD',
|
||||||
|
sales_usd_180d: '180G Toplam Ciro USD',
|
||||||
|
sales_usd_365d: '360G Toplam Ciro USD',
|
||||||
|
sales_usd_total: 'Genel Toplam Ciro USD',
|
||||||
|
avg_price_usd: 'Ağ. Ort. Fiyat USD',
|
||||||
|
avg_price_usd_90d: '90G Ağ. Ort. Fiyat USD',
|
||||||
|
avg_price_usd_180d: '180G Ağ. Ort. Fiyat USD',
|
||||||
|
avg_price_usd_365d: '360G Ağ. Ort. Fiyat USD',
|
||||||
|
avg_price_usd_total: 'Genel Ağ. Ort. Fiyat USD',
|
||||||
|
base_price_usd: 'Ort. Taban Maliyet USD',
|
||||||
|
cost_price_usd: 'Ort. Çıplak Maliyet USD',
|
||||||
|
base_price_usd_90d: '90G Ort. Taban Maliyet USD',
|
||||||
|
cost_price_usd_90d: '90G Ort. Çıplak Maliyet USD',
|
||||||
|
base_price_usd_180d: '180G Ort. Taban Maliyet USD',
|
||||||
|
cost_price_usd_180d: '180G Ort. Çıplak Maliyet USD',
|
||||||
|
base_price_usd_365d: '360G Ort. Taban Maliyet USD',
|
||||||
|
cost_price_usd_365d: '360G Ort. Çıplak Maliyet USD',
|
||||||
|
base_price_usd_total: 'Genel Ort. Taban Maliyet USD',
|
||||||
|
cost_price_usd_total: 'Genel Ort. Çıplak Maliyet USD',
|
||||||
|
unit_profit_base_90d: '90G Ağ. Ort. Birim Taban K/Z USD',
|
||||||
|
unit_profit_cost_90d: '90G Ağ. Ort. Birim Çıplak K/Z USD',
|
||||||
|
unit_profit_base_180d: '180G Ağ. Ort. Birim Taban K/Z USD',
|
||||||
|
unit_profit_cost_180d: '180G Ağ. Ort. Birim Çıplak K/Z USD',
|
||||||
|
unit_profit_base_total: 'Genel Ağ. Ort. Birim Taban K/Z USD',
|
||||||
|
unit_profit_cost_total: 'Genel Ağ. Ort. Birim Çıplak K/Z USD',
|
||||||
|
gross_profit_usd_90d: '90G Toplam K/Z USD',
|
||||||
|
gross_profit_usd_180d: '180G Toplam K/Z USD',
|
||||||
|
gross_profit_usd_total: 'Genel Toplam K/Z USD',
|
||||||
|
gross_profit_base_usd_90d: '90G Taban Toplam K/Z USD',
|
||||||
|
gross_profit_cost_usd_90d: '90G Çıplak Toplam K/Z USD',
|
||||||
|
gross_profit_base_usd_180d: '180G Taban Toplam K/Z USD',
|
||||||
|
gross_profit_cost_usd_180d: '180G Çıplak Toplam K/Z USD',
|
||||||
|
gross_profit_base_usd_365d: '360G Taban Toplam K/Z USD',
|
||||||
|
gross_profit_cost_usd_365d: '360G Çıplak Toplam K/Z USD',
|
||||||
|
gross_profit_base_usd_total: 'Genel Taban Toplam K/Z USD',
|
||||||
|
gross_profit_cost_usd_total: 'Genel Çıplak Toplam K/Z USD',
|
||||||
|
gross_margin_base_90d: '90G Ağ. Ort. Taban Marj',
|
||||||
|
gross_margin_cost_90d: '90G Ağ. Ort. Çıplak Marj',
|
||||||
|
gross_margin_base_180d: '180G Ağ. Ort. Taban Marj',
|
||||||
|
gross_margin_cost_180d: '180G Ağ. Ort. Çıplak Marj',
|
||||||
|
gross_margin_base_365d: '360G Ağ. Ort. Taban Marj',
|
||||||
|
gross_margin_cost_365d: '360G Ağ. Ort. Çıplak Marj',
|
||||||
|
gross_margin_base_total: 'Genel Ağ. Ort. Taban Marj',
|
||||||
|
gross_margin_cost_total: 'Genel Ağ. Ort. Çıplak Marj',
|
||||||
|
performance_score: 'Ağ. Ort. Skor',
|
||||||
|
performance_score_90d: '90G Ağ. Ort. Ürün Skor',
|
||||||
|
performance_score_180d: '180G Ağ. Ort. Ürün Skor',
|
||||||
|
performance_score_365d: '360G Ağ. Ort. Ürün Skor',
|
||||||
|
performance_score_total: 'Genel Ağ. Ort. Ürün Skor',
|
||||||
|
customer_score_90d: '90G Ağ. Ort. Müşteri Skor',
|
||||||
|
customer_score_180d: '180G Ağ. Ort. Müşteri Skor',
|
||||||
|
customer_score_365d: '360G Ağ. Ort. Müşteri Skor',
|
||||||
|
customer_score_total: 'Genel Ağ. Ort. Müşteri Skor',
|
||||||
|
market_count_90d: '90G Tekil Piyasa',
|
||||||
|
customer_count_90d: '90G Tekil Müşteri',
|
||||||
|
market_count_total: 'Genel Tekil Piyasa',
|
||||||
|
customer_count_total: 'Genel Tekil Müşteri',
|
||||||
|
sales_index_90d: '90G Ağ. Ort. Endeks',
|
||||||
|
sales_index_total: 'Genel Ağ. Ort. Endeks',
|
||||||
|
stock_days_90d: '90G Ağ. Ort. Stok Gün',
|
||||||
|
stock_days_180d: '180G Ağ. Ort. Stok Gün',
|
||||||
|
stock_days_365d: '360G Ağ. Ort. Stok Gün',
|
||||||
|
stock_days_total: 'Genel Ağ. Ort. Stok Gün',
|
||||||
|
avg_daily_sales_total: 'Genel Günlük Ort. Adet'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMetricLabelOverrides (...columnGroups) {
|
||||||
|
for (const group of columnGroups) {
|
||||||
|
for (const col of group || []) {
|
||||||
|
if (metricLabelOverrides[col?.name]) col.label = metricLabelOverrides[col.name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMetricLabelOverrides(
|
||||||
|
detailSalesColumns,
|
||||||
|
columns,
|
||||||
|
generalColumns,
|
||||||
|
orderAnalysisColumns,
|
||||||
|
orderGroupColumns,
|
||||||
|
orderProductCustomerColumns,
|
||||||
|
orderMarketDetailColumns,
|
||||||
|
idleColumns,
|
||||||
|
marketColumns,
|
||||||
|
countryColumns,
|
||||||
|
customerColumns,
|
||||||
|
salesBreakdownColumns,
|
||||||
|
productPeriodScoreColumns,
|
||||||
|
customerPeriodScoreColumns
|
||||||
|
)
|
||||||
|
|
||||||
function ensureColumns (targetColumns, columnsToEnsure) {
|
function ensureColumns (targetColumns, columnsToEnsure) {
|
||||||
const existing = new Set(targetColumns.map(col => col.name))
|
const existing = new Set(targetColumns.map(col => col.name))
|
||||||
for (const col of columnsToEnsure) {
|
for (const col of columnsToEnsure) {
|
||||||
@@ -2922,7 +3035,7 @@ function backendGroupedFilterOptionParams (tabKey) {
|
|||||||
mode: tabKey,
|
mode: tabKey,
|
||||||
groupLevels: (tabGroupLevels[tabKey] || groupLevels).map(level => level.key),
|
groupLevels: (tabGroupLevels[tabKey] || groupLevels).map(level => level.key),
|
||||||
fields,
|
fields,
|
||||||
limit: 5000
|
limit: 50000
|
||||||
}
|
}
|
||||||
if (tabKey === 'product_detail') params.urunAnaGrubu = selectedDetailMainGroup.value
|
if (tabKey === 'product_detail') params.urunAnaGrubu = selectedDetailMainGroup.value
|
||||||
return params
|
return params
|
||||||
@@ -3283,7 +3396,9 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
|
|||||||
sales_index_total: averageRows(groupRows, 'sales_index_total'),
|
sales_index_total: averageRows(groupRows, 'sales_index_total'),
|
||||||
performance_bucket: dominantValue(groupRows, 'performance_bucket'),
|
performance_bucket: dominantValue(groupRows, 'performance_bucket'),
|
||||||
...aggregate,
|
...aggregate,
|
||||||
performance_score: groupPerformanceScore(aggregate, groupDef.key),
|
performance_score: Object.prototype.hasOwnProperty.call(aggregate, 'performance_score')
|
||||||
|
? aggregate.performance_score
|
||||||
|
: groupPerformanceScore(aggregate, groupDef.key),
|
||||||
recommendation: dominantValue(groupRows, 'recommendation') || `${formatNumber(groupRows.length, 0)} satır`
|
recommendation: dominantValue(groupRows, 'recommendation') || `${formatNumber(groupRows.length, 0)} satır`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3372,15 +3487,15 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
|||||||
if (sourceRows.some(row => row.performance_bucket)) {
|
if (sourceRows.some(row => row.performance_bucket)) {
|
||||||
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
|
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
|
||||||
}
|
}
|
||||||
out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_90d')) out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
|
||||||
out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_180d')) out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
|
||||||
out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_365d')) out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
|
||||||
out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_total')) out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
|
||||||
out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')) out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
|
||||||
out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_180d')) out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
|
||||||
out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_365d')) out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
|
||||||
out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
|
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_total')) out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
|
||||||
out.performance_score = groupPerformanceScore(out, groupField)
|
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score')) out.performance_score = groupPerformanceScore(out, groupField)
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupPerformanceScore (row, groupField = '') {
|
function groupPerformanceScore (row, groupField = '') {
|
||||||
@@ -3672,18 +3787,12 @@ function weightedAverageProductCost (sourceRows, valueField) {
|
|||||||
if (!key || variants.has(key)) continue
|
if (!key || variants.has(key)) continue
|
||||||
variants.set(key, row)
|
variants.set(key, row)
|
||||||
}
|
}
|
||||||
const stockSource = variants.size > 0 ? Array.from(variants.values()) : sourceRows
|
const averageSource = variants.size > 0 ? Array.from(variants.values()) : sourceRows
|
||||||
const stockRows = stockSource
|
const values = averageSource
|
||||||
.map(row => ({
|
.map(row => Number(row[valueField] || 0))
|
||||||
value: Number(row[valueField] || 0),
|
.filter(value => Number.isFinite(value) && value > 0)
|
||||||
qty: Number(row.stock_qty || 0)
|
if (!values.length) return 0
|
||||||
}))
|
return values.reduce((sum, value) => sum + value, 0) / values.length
|
||||||
.filter(row => Number.isFinite(row.value) && Number.isFinite(row.qty) && row.value > 0 && row.qty > 0)
|
|
||||||
const stockQty = stockRows.reduce((sum, row) => sum + row.qty, 0)
|
|
||||||
if (stockQty > 0) {
|
|
||||||
return stockRows.reduce((sum, row) => sum + row.value * row.qty, 0) / stockQty
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function marginFromSalesCost (salesUSD, qty, unitCost) {
|
function marginFromSalesCost (salesUSD, qty, unitCost) {
|
||||||
@@ -3878,27 +3987,50 @@ function formatGroupCell (row, colOrName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withProductMargins (row) {
|
function withProductMargins (row) {
|
||||||
|
const salesQtyTotal = Number(row?.sales_qty_total || 0)
|
||||||
|
const salesUSDTotal = Number(row?.sales_usd_total || 0)
|
||||||
|
const basePrice = Number(row?.base_price_usd || 0)
|
||||||
|
const costPrice = Number(row?.cost_price_usd || 0)
|
||||||
|
const grossProfitBaseTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * basePrice) : 0
|
||||||
|
const grossProfitCostTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * costPrice) : 0
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
avg_price_usd_365d: Number(row?.sales_qty_365d || 0) > 0 ? Number(row?.sales_usd_365d || 0) / Number(row?.sales_qty_365d || 0) : 0,
|
avg_price_usd_365d: Number(row?.sales_qty_365d || 0) > 0 ? Number(row?.sales_usd_365d || 0) / Number(row?.sales_qty_365d || 0) : 0,
|
||||||
|
avg_price_usd_total: salesQtyTotal > 0 ? salesUSDTotal / salesQtyTotal : Number(row?.avg_price_usd_total || 0),
|
||||||
stock_turnover_90d: existingOrStockTurnover(row?.stock_turnover_90d, row?.sales_qty_90d, row?.stock_qty),
|
stock_turnover_90d: existingOrStockTurnover(row?.stock_turnover_90d, row?.sales_qty_90d, row?.stock_qty),
|
||||||
stock_turnover_180d: existingOrStockTurnover(row?.stock_turnover_180d, row?.sales_qty_180d, row?.stock_qty),
|
stock_turnover_180d: existingOrStockTurnover(row?.stock_turnover_180d, row?.sales_qty_180d, row?.stock_qty),
|
||||||
stock_turnover_365d: existingOrStockTurnover(row?.stock_turnover_365d, row?.sales_qty_365d, row?.stock_qty),
|
stock_turnover_365d: existingOrStockTurnover(row?.stock_turnover_365d, row?.sales_qty_365d, row?.stock_qty),
|
||||||
|
stock_turnover_total: existingOrStockTurnover(row?.stock_turnover_total, row?.sales_qty_total, row?.stock_qty),
|
||||||
|
gross_profit_base_usd_total: grossProfitBaseTotal,
|
||||||
|
gross_profit_cost_usd_total: grossProfitCostTotal,
|
||||||
|
gross_profit_usd_total: Number(row?.gross_profit_usd_total ?? grossProfitCostTotal),
|
||||||
gross_margin_base_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.base_price_usd),
|
gross_margin_base_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.base_price_usd),
|
||||||
gross_margin_cost_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.cost_price_usd),
|
gross_margin_cost_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.cost_price_usd),
|
||||||
gross_margin_base_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.base_price_usd),
|
gross_margin_base_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.base_price_usd),
|
||||||
gross_margin_cost_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.cost_price_usd),
|
gross_margin_cost_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.cost_price_usd),
|
||||||
gross_margin_base_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.base_price_usd),
|
gross_margin_base_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.base_price_usd),
|
||||||
gross_margin_cost_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.cost_price_usd)
|
gross_margin_cost_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.cost_price_usd),
|
||||||
|
gross_margin_base_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, basePrice),
|
||||||
|
gross_margin_cost_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, costPrice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withGeneralMargins (row) {
|
function withGeneralMargins (row) {
|
||||||
|
const salesQtyTotal = Number(row?.sales_qty_total || 0)
|
||||||
|
const salesUSDTotal = Number(row?.sales_usd_total || 0)
|
||||||
|
const basePrice = Number(row?.base_price_usd || 0)
|
||||||
|
const costPrice = Number(row?.cost_price_usd || 0)
|
||||||
|
const grossProfitBaseTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * basePrice) : 0
|
||||||
|
const grossProfitCostTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * costPrice) : 0
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
|
avg_price_usd_total: salesQtyTotal > 0 ? salesUSDTotal / salesQtyTotal : Number(row?.avg_price_usd_total || 0),
|
||||||
stock_turnover_total: existingOrStockTurnover(row?.stock_turnover_total, row?.sales_qty_total, row?.stock_qty),
|
stock_turnover_total: existingOrStockTurnover(row?.stock_turnover_total, row?.sales_qty_total, row?.stock_qty),
|
||||||
gross_margin_base_total: marginFromSalesCost(row?.sales_usd_total, row?.sales_qty_total, row?.base_price_usd),
|
gross_profit_base_usd_total: grossProfitBaseTotal,
|
||||||
gross_margin_cost_total: marginFromSalesCost(row?.sales_usd_total, row?.sales_qty_total, row?.cost_price_usd)
|
gross_profit_cost_usd_total: grossProfitCostTotal,
|
||||||
|
gross_profit_usd_total: Number(row?.gross_profit_usd_total ?? grossProfitCostTotal),
|
||||||
|
gross_margin_base_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, basePrice),
|
||||||
|
gross_margin_cost_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, costPrice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4276,19 +4408,19 @@ function normalizeRow (row) {
|
|||||||
const color = String(row?.color_code || '').trim()
|
const color = String(row?.color_code || '').trim()
|
||||||
const yaka = String(row?.yaka_kodu || '').trim()
|
const yaka = String(row?.yaka_kodu || '').trim()
|
||||||
const market = String(row?.market_key || '').trim()
|
const market = String(row?.market_key || '').trim()
|
||||||
return withPeriodScores(withProductMargins({
|
return withPeriodScores(withGeneralMargins(withProductMargins({
|
||||||
...row,
|
...row,
|
||||||
row_key: `${product}|${color}|${yaka}|${market}`
|
row_key: `${product}|${color}|${yaka}|${market}`
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPeriodScores (row) {
|
function withPeriodScores (row) {
|
||||||
const next = { ...row }
|
const next = { ...row }
|
||||||
next.performance_score_90d = Number(next.performance_score_90d || productSalesPeriodScore(productPeriodMetricSource(next, '90d')))
|
if (!Object.prototype.hasOwnProperty.call(next, 'performance_score_90d')) next.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(next, '90d'))
|
||||||
next.performance_score_180d = Number(next.performance_score_180d || productSalesPeriodScore(productPeriodMetricSource(next, '180d')))
|
if (!Object.prototype.hasOwnProperty.call(next, 'performance_score_180d')) next.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(next, '180d'))
|
||||||
next.performance_score_365d = Number(next.performance_score_365d || productSalesPeriodScore(productPeriodMetricSource(next, '365d')))
|
if (!Object.prototype.hasOwnProperty.call(next, 'performance_score_365d')) next.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(next, '365d'))
|
||||||
next.performance_score_total = Number(next.performance_score_total || productSalesPeriodScore(productPeriodMetricSource(next, 'total')))
|
if (!Object.prototype.hasOwnProperty.call(next, 'performance_score_total')) next.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(next, 'total'))
|
||||||
if (!Number(next.performance_score || 0)) next.performance_score = next.performance_score_90d
|
if (!Object.prototype.hasOwnProperty.call(next, 'performance_score')) next.performance_score = next.performance_score_90d
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4422,16 +4554,6 @@ function displayMarketName (value) {
|
|||||||
return parts.length ? parts[parts.length - 1] : ''
|
return parts.length ? parts[parts.length - 1] : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUploadsPath (storagePath) {
|
|
||||||
const raw = String(storagePath || '').trim()
|
|
||||||
if (!raw) return ''
|
|
||||||
const normalized = raw.replace(/\\/g, '/')
|
|
||||||
const idx = normalized.toLowerCase().indexOf('/uploads/')
|
|
||||||
if (idx >= 0) return normalized.slice(idx)
|
|
||||||
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProductImageUrl (item) {
|
function resolveProductImageUrl (item) {
|
||||||
return resolveProductImageUrls(item)[0] || ''
|
return resolveProductImageUrls(item)[0] || ''
|
||||||
}
|
}
|
||||||
@@ -4450,18 +4572,9 @@ function resolveProductImageUrls (item) {
|
|||||||
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
||||||
addURL(fullURL)
|
addURL(fullURL)
|
||||||
|
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
|
||||||
addURL(uploadsPath)
|
|
||||||
|
|
||||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
|
||||||
if (fileName) addURL(`/uploads/image/${fileName}`)
|
|
||||||
|
|
||||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||||
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||||
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||||
|
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
|
||||||
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
|
||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4897,6 +5010,72 @@ async function timedProductPerformanceGet (label, url, config = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function productPerformanceExcelExportTableKey () {
|
||||||
|
if (isProductKpiTab.value) return activeProductTableKey.value
|
||||||
|
if (backendGroupedSupportedTab(activeTab.value)) return activeTab.value
|
||||||
|
return 'products'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProductPerformanceExcelExportFilters () {
|
||||||
|
const tableKey = productPerformanceExcelExportTableKey()
|
||||||
|
const out = {}
|
||||||
|
for (const col of columnsForTableKey(tableKey) || []) {
|
||||||
|
const name = String(col?.name || '').trim()
|
||||||
|
if (!productPerformanceExcelExportFilterFields.has(name)) continue
|
||||||
|
const selected = selectedColumnFilters(tableKey, name)
|
||||||
|
if (selected.length) out[name] = selected
|
||||||
|
}
|
||||||
|
if (activeTab.value === 'product_detail') {
|
||||||
|
const mainGroup = String(selectedDetailMainGroup.value || '').trim()
|
||||||
|
if (mainGroup && !out.urun_ana_grubu?.includes(mainGroup)) {
|
||||||
|
out.urun_ana_grubu = [...(out.urun_ana_grubu || []), mainGroup]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function productPerformanceExcelFilename (headers = {}) {
|
||||||
|
const disposition = String(headers?.['content-disposition'] || headers?.['Content-Disposition'] || '').trim()
|
||||||
|
const utfMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i)
|
||||||
|
if (utfMatch?.[1]) return decodeURIComponent(utfMatch[1].replaceAll('"', '').trim())
|
||||||
|
const match = disposition.match(/filename="?([^";]+)"?/i)
|
||||||
|
return match?.[1]?.trim() || `product_performance_color_yaka_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadProductPerformanceExcelBlob (blob, filename) {
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportProductPerformanceExcel () {
|
||||||
|
if (excelExportLoading.value) return
|
||||||
|
excelExportLoading.value = true
|
||||||
|
try {
|
||||||
|
const tableKey = productPerformanceExcelExportTableKey()
|
||||||
|
const pagination = tablePagination[tableKey] || tablePagination.products || {}
|
||||||
|
const response = await api.post('/pricing/product-performance/export-excel', {
|
||||||
|
filters: buildProductPerformanceExcelExportFilters(),
|
||||||
|
sort_by: pagination.sortBy || '',
|
||||||
|
descending: pagination.descending !== false
|
||||||
|
}, {
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 180000
|
||||||
|
})
|
||||||
|
downloadProductPerformanceExcelBlob(response.data, productPerformanceExcelFilename(response.headers || {}))
|
||||||
|
} catch (err) {
|
||||||
|
const detail = await extractApiErrorDetail(err)
|
||||||
|
Notify.create({ type: 'negative', message: detail || 'Excel çıktısı alınamadı' })
|
||||||
|
} finally {
|
||||||
|
excelExportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function reload () {
|
async function reload () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
startLoadingTimer('Rapor verileri hazırlanıyor...')
|
startLoadingTimer('Rapor verileri hazırlanıyor...')
|
||||||
|
|||||||
@@ -637,14 +637,9 @@ function resolveProductImageUrl(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contentUrl = ''
|
let contentUrl = ''
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
|
||||||
if (Number.isFinite(imageId) && imageId > 0) {
|
|
||||||
contentUrl = `/api/product-images/${imageId}/content`
|
|
||||||
} else {
|
|
||||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||||
if (contentURL.startsWith('/api/')) contentUrl = contentURL
|
if (contentURL.startsWith('/api/')) contentUrl = contentURL
|
||||||
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
|
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
|
||||||
}
|
|
||||||
|
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
||||||
let publicUrl = ''
|
let publicUrl = ''
|
||||||
@@ -707,6 +702,9 @@ function clearGalleryQueryIndex() {
|
|||||||
|
|
||||||
async function resolveProductImageUrlForCarousel(item) {
|
async function resolveProductImageUrlForCarousel(item) {
|
||||||
const resolved = resolveProductImageUrl(item)
|
const resolved = resolveProductImageUrl(item)
|
||||||
|
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
|
||||||
|
if (directUrl) return directUrl
|
||||||
|
|
||||||
const contentUrl = String(resolved.contentUrl || '').trim()
|
const contentUrl = String(resolved.contentUrl || '').trim()
|
||||||
if (contentUrl) {
|
if (contentUrl) {
|
||||||
try {
|
try {
|
||||||
@@ -721,10 +719,7 @@ async function resolveProductImageUrlForCarousel(item) {
|
|||||||
// fall through to public url
|
// fall through to public url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fullUrl = String(resolved.fullUrl || '').trim()
|
return contentUrl
|
||||||
if (fullUrl) return fullUrl
|
|
||||||
const publicUrl = String(resolved.publicUrl || '').trim()
|
|
||||||
return String(publicUrl || fullUrl || contentUrl || '').trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
||||||
|
|||||||
@@ -618,14 +618,9 @@ function resolveProductImageUrl(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contentUrl = ''
|
let contentUrl = ''
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
|
||||||
if (Number.isFinite(imageId) && imageId > 0) {
|
|
||||||
contentUrl = `/api/product-images/${imageId}/content`
|
|
||||||
} else {
|
|
||||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||||
if (contentURL.startsWith('/api/')) contentUrl = contentURL
|
if (contentURL.startsWith('/api/')) contentUrl = contentURL
|
||||||
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
|
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
|
||||||
}
|
|
||||||
|
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
||||||
let publicUrl = ''
|
let publicUrl = ''
|
||||||
@@ -686,6 +681,9 @@ function clearGalleryQueryIndex() {
|
|||||||
|
|
||||||
async function resolveProductImageUrlForCarousel(item) {
|
async function resolveProductImageUrlForCarousel(item) {
|
||||||
const resolved = resolveProductImageUrl(item)
|
const resolved = resolveProductImageUrl(item)
|
||||||
|
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
|
||||||
|
if (directUrl) return directUrl
|
||||||
|
|
||||||
const contentUrl = String(resolved.contentUrl || '').trim()
|
const contentUrl = String(resolved.contentUrl || '').trim()
|
||||||
if (contentUrl) {
|
if (contentUrl) {
|
||||||
try {
|
try {
|
||||||
@@ -700,10 +698,7 @@ async function resolveProductImageUrlForCarousel(item) {
|
|||||||
// fall through to public url
|
// fall through to public url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fullUrl = String(resolved.fullUrl || '').trim()
|
return contentUrl
|
||||||
if (fullUrl) return fullUrl
|
|
||||||
const publicUrl = String(resolved.publicUrl || '').trim()
|
|
||||||
return String(publicUrl || fullUrl || contentUrl || '').trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ import api from 'src/services/api'
|
|||||||
const GROUPED_TTL_MS = 2 * 60 * 1000
|
const GROUPED_TTL_MS = 2 * 60 * 1000
|
||||||
const IMAGE_TTL_MS = 15 * 60 * 1000
|
const IMAGE_TTL_MS = 15 * 60 * 1000
|
||||||
|
|
||||||
function normalizeUploadsPath (storagePath) {
|
|
||||||
const raw = String(storagePath || '').trim()
|
|
||||||
if (!raw) return ''
|
|
||||||
const normalized = raw.replace(/\\/g, '/')
|
|
||||||
const idx = normalized.toLowerCase().indexOf('/uploads/')
|
|
||||||
if (idx >= 0) return normalized.slice(idx)
|
|
||||||
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProductImageUrl (item) {
|
function resolveProductImageUrl (item) {
|
||||||
return resolveProductImageUrls(item)[0] || ''
|
return resolveProductImageUrls(item)[0] || ''
|
||||||
}
|
}
|
||||||
@@ -32,18 +22,9 @@ function resolveProductImageUrls (item) {
|
|||||||
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
||||||
addURL(fullURL)
|
addURL(fullURL)
|
||||||
|
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
|
||||||
addURL(uploadsPath)
|
|
||||||
|
|
||||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
|
||||||
if (fileName) addURL(`/uploads/image/${fileName}`)
|
|
||||||
|
|
||||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||||
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||||
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||||
|
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
|
||||||
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
|
||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +187,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
||||||
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
||||||
fields: Array.isArray(params.fields) ? params.fields : String(params.fields || '').split(',').filter(Boolean),
|
fields: Array.isArray(params.fields) ? params.fields : String(params.fields || '').split(',').filter(Boolean),
|
||||||
limit: params.limit || 5000
|
limit: params.limit || 50000
|
||||||
}, { timeout: 60000 }).then(resp => {
|
}, { timeout: 60000 }).then(resp => {
|
||||||
const data = resp?.data && typeof resp.data === 'object' ? resp.data : {}
|
const data = resp?.data && typeof resp.data === 'object' ? resp.data : {}
|
||||||
this.groupedFilterOptionsByKey = { ...this.groupedFilterOptionsByKey, [key]: data }
|
this.groupedFilterOptionsByKey = { ...this.groupedFilterOptionsByKey, [key]: data }
|
||||||
|
|||||||
Reference in New Issue
Block a user