Fix product performance grouped JSON build
This commit is contained in:
@@ -3465,11 +3465,13 @@ LIMIT $1
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProductPerformanceGroupedRequest struct {
|
type ProductPerformanceGroupedRequest struct {
|
||||||
Mode string
|
Mode string
|
||||||
GroupLevels []string
|
GroupLevels []string
|
||||||
ExpandedKeys map[string]bool
|
ExpandedKeys map[string]bool
|
||||||
Limit int
|
ExpandThroughLevel int
|
||||||
MainGroup string
|
Limit int
|
||||||
|
MainGroup string
|
||||||
|
Filters map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
||||||
@@ -3480,6 +3482,9 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|||||||
if len(levels) == 0 {
|
if len(levels) == 0 {
|
||||||
levels = defaultProductPerformanceGroupLevels(req.Mode)
|
levels = defaultProductPerformanceGroupLevels(req.Mode)
|
||||||
}
|
}
|
||||||
|
if req.ExpandThroughLevel > len(levels)-2 {
|
||||||
|
req.ExpandThroughLevel = len(levels) - 2
|
||||||
|
}
|
||||||
|
|
||||||
if out, ok, err := listProductPerformanceGroupedSQL(ctx, pg, req, levels); ok || err != nil {
|
if out, ok, err := listProductPerformanceGroupedSQL(ctx, pg, req, levels); ok || err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
@@ -3489,14 +3494,16 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
sourceRows = filterProductPerformanceGroupedRows(sourceRows, req.Filters)
|
||||||
out := make([]map[string]any, 0, len(sourceRows))
|
out := make([]map[string]any, 0, len(sourceRows))
|
||||||
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys)
|
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type productPerformanceSQLGroupFilter struct {
|
type productPerformanceSQLGroupFilter struct {
|
||||||
Field string
|
Field string
|
||||||
Value string
|
Value string
|
||||||
|
Values []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
||||||
@@ -3509,25 +3516,28 @@ func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req Produ
|
|||||||
}
|
}
|
||||||
out := make([]map[string]any, 0, 512)
|
out := make([]map[string]any, 0, 512)
|
||||||
filters := productPerformanceGroupedBaseFilters(req)
|
filters := productPerformanceGroupedBaseFilters(req)
|
||||||
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.Limit)
|
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
||||||
return out, true, err
|
return out, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
||||||
|
filters := make([]productPerformanceSQLGroupFilter, 0, len(req.Filters)+1)
|
||||||
mainGroup := strings.TrimSpace(req.MainGroup)
|
mainGroup := strings.TrimSpace(req.MainGroup)
|
||||||
if strings.TrimSpace(req.Mode) != "product_detail" || mainGroup == "" {
|
if strings.TrimSpace(req.Mode) == "product_detail" && mainGroup != "" {
|
||||||
return nil
|
filters = append(filters, productPerformanceSQLGroupFilter{Field: "urun_ana_grubu", Value: mainGroup})
|
||||||
}
|
}
|
||||||
return []productPerformanceSQLGroupFilter{{Field: "urun_ana_grubu", Value: mainGroup}}
|
for field, values := range req.Filters {
|
||||||
|
cleanValues := cleanProductPerformanceFilterValues(values)
|
||||||
|
if len(cleanValues) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filters = append(filters, productPerformanceSQLGroupFilter{Field: field, Values: cleanValues})
|
||||||
|
}
|
||||||
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, limit int) error {
|
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, expandThroughLevel int, limit int) error {
|
||||||
if level >= len(levels) {
|
if level >= len(levels) {
|
||||||
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*out = append(*out, rows...)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3540,7 +3550,7 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out
|
|||||||
value := stringFromMap(row, "group_value")
|
value := stringFromMap(row, "group_value")
|
||||||
if shouldSkipProductPerformanceGroupValue(field, value) {
|
if shouldSkipProductPerformanceGroupValue(field, value) {
|
||||||
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
|
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
|
||||||
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel, parentKeys, nextFilters, expandedKeys, limit); err != nil {
|
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel, parentKeys, nextFilters, expandedKeys, expandThroughLevel, limit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -3556,9 +3566,9 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out
|
|||||||
row["__group"] = true
|
row["__group"] = true
|
||||||
row[field] = value
|
row[field] = value
|
||||||
*out = append(*out, row)
|
*out = append(*out, row)
|
||||||
if expandedKeys[key] {
|
if expandedKeys[key] || level <= expandThroughLevel {
|
||||||
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
|
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
|
||||||
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel+1, append(parentKeys, keyPart), nextFilters, expandedKeys, limit); err != nil {
|
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel+1, append(parentKeys, keyPart), nextFilters, expandedKeys, expandThroughLevel, limit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3889,7 +3899,9 @@ func productPerformanceSQLGroupExpr(field string) (string, bool) {
|
|||||||
return "CASE WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(urun_ilk_grubu,'') END", true
|
return "CASE WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(urun_ilk_grubu,'') END", true
|
||||||
case "askili_yan":
|
case "askili_yan":
|
||||||
return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true
|
return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true
|
||||||
case "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu":
|
case "urun_ana_grubu":
|
||||||
|
return "COALESCE(NULLIF(btrim(COALESCE(urun_ana_grubu,'')), ''), NULLIF(btrim(COALESCE(urun_alt_grubu,'')), ''), NULLIF(btrim(COALESCE(kategori,'')), ''), '')", true
|
||||||
|
case "kategori", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu", "performance_bucket":
|
||||||
return field, true
|
return field, true
|
||||||
case "color_yaka":
|
case "color_yaka":
|
||||||
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
|
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
|
||||||
@@ -3920,12 +3932,93 @@ func productPerformanceSQLFilterWhere(filters []productPerformanceSQLGroupFilter
|
|||||||
if !ok {
|
if !ok {
|
||||||
return "", nil, fmt.Errorf("unsupported product performance filter field: %s", filter.Field)
|
return "", nil, fmt.Errorf("unsupported product performance filter field: %s", filter.Field)
|
||||||
}
|
}
|
||||||
args = append(args, normalizeProductPerformanceGroupValue(filter.Value))
|
values := filter.Values
|
||||||
parts = append(parts, fmt.Sprintf("COALESCE(%s, '') = $%d", expr, len(args)))
|
if len(values) == 0 && strings.TrimSpace(filter.Value) != "" {
|
||||||
|
values = []string{filter.Value}
|
||||||
|
}
|
||||||
|
values = cleanProductPerformanceFilterValues(values)
|
||||||
|
if len(values) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
args = append(args, pq.Array(values))
|
||||||
|
parts = append(parts, fmt.Sprintf("COALESCE(%s, '') = ANY($%d)", expr, len(args)))
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
return "WHERE " + strings.Join(parts, " AND "), args, nil
|
return "WHERE " + strings.Join(parts, " AND "), args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanProductPerformanceFilterValues(values []string) []string {
|
||||||
|
out := make([]string, 0, len(values))
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, value := range values {
|
||||||
|
clean := normalizeProductPerformanceGroupValue(value)
|
||||||
|
if clean == "" || seen[clean] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[clean] = true
|
||||||
|
out = append(out, clean)
|
||||||
|
if len(out) >= 300 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterProductPerformanceGroupedRows(rows []map[string]any, filters map[string][]string) []map[string]any {
|
||||||
|
if len(filters) == 0 || len(rows) == 0 {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
cleanFilters := map[string]map[string]bool{}
|
||||||
|
for field, values := range filters {
|
||||||
|
cleanValues := cleanProductPerformanceFilterValues(values)
|
||||||
|
if len(cleanValues) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set := map[string]bool{}
|
||||||
|
for _, value := range cleanValues {
|
||||||
|
set[value] = true
|
||||||
|
}
|
||||||
|
cleanFilters[field] = set
|
||||||
|
}
|
||||||
|
if len(cleanFilters) == 0 {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
out := make([]map[string]any, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
matches := true
|
||||||
|
for field, allowed := range cleanFilters {
|
||||||
|
if !allowed[productPerformanceGroupedFilterValue(row, field)] {
|
||||||
|
matches = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches {
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupedFilterValue(row map[string]any, field string) string {
|
||||||
|
switch field {
|
||||||
|
case "urun_ana_grubu":
|
||||||
|
value := strings.TrimSpace(stringFromMap(row, "urun_ana_grubu"))
|
||||||
|
if value != "" {
|
||||||
|
return normalizeProductPerformanceGroupValue(value)
|
||||||
|
}
|
||||||
|
if value = strings.TrimSpace(stringFromMap(row, "urun_alt_grubu")); value != "" {
|
||||||
|
return normalizeProductPerformanceGroupValue(value)
|
||||||
|
}
|
||||||
|
return normalizeProductPerformanceGroupValue(stringFromMap(row, "kategori"))
|
||||||
|
case "market_key", "color_yaka", "urun_ilk_grubu", "askili_yan":
|
||||||
|
return normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
|
||||||
|
default:
|
||||||
|
return normalizeProductPerformanceGroupValue(stringFromMap(row, field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
|
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
|
||||||
switch mode {
|
switch mode {
|
||||||
case "products":
|
case "products":
|
||||||
@@ -3986,9 +4079,8 @@ func productPerformanceIdleSourceRows(rows []map[string]any) []map[string]any {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, visualLevel int, parentKeys []string, expandedKeys map[string]bool) {
|
func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, visualLevel int, parentKeys []string, expandedKeys map[string]bool, expandThroughLevel int) {
|
||||||
if level >= len(levels) {
|
if level >= len(levels) {
|
||||||
*out = append(*out, sourceRows...)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4010,14 +4102,14 @@ func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map
|
|||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
groupRows := grouped[value]
|
groupRows := grouped[value]
|
||||||
if shouldSkipProductPerformanceGroupValue(field, value) {
|
if shouldSkipProductPerformanceGroupValue(field, value) {
|
||||||
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel, parentKeys, expandedKeys)
|
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel, parentKeys, expandedKeys, expandThroughLevel)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keyPart := field + ":" + value
|
keyPart := field + ":" + value
|
||||||
key := strings.Join(append(parentKeys, keyPart), "|")
|
key := strings.Join(append(parentKeys, keyPart), "|")
|
||||||
*out = append(*out, makeProductPerformanceGroupedRow(key, visualLevel, field, value, groupRows))
|
*out = append(*out, makeProductPerformanceGroupedRow(key, visualLevel, field, value, groupRows))
|
||||||
if expandedKeys[key] {
|
if expandedKeys[key] || level <= expandThroughLevel {
|
||||||
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel+1, append(parentKeys, keyPart), expandedKeys)
|
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel+1, append(parentKeys, keyPart), expandedKeys, expandThroughLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4076,6 +4168,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
if key == "row_key" || key == "key" {
|
if key == "row_key" || key == "key" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if isProductPerformanceMarginField(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if key == "stock_qty" {
|
if key == "stock_qty" {
|
||||||
out[key] = distinctProductPerformanceVariantStockQty(rows)
|
out[key] = distinctProductPerformanceVariantStockQty(rows)
|
||||||
continue
|
continue
|
||||||
@@ -4093,6 +4188,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
for key := range row {
|
for key := range row {
|
||||||
|
if isProductPerformanceMarginField(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if shouldAverageProductPerformanceField(key) {
|
if shouldAverageProductPerformanceField(key) {
|
||||||
out[key] = weightedAverageProductPerformanceRows(rows, key, productPerformanceMetricWeightField(key))
|
out[key] = weightedAverageProductPerformanceRows(rows, key, productPerformanceMetricWeightField(key))
|
||||||
}
|
}
|
||||||
@@ -4103,6 +4201,10 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isProductPerformanceMarginField(field string) bool {
|
||||||
|
return strings.HasPrefix(field, "gross_margin")
|
||||||
|
}
|
||||||
|
|
||||||
func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
|
func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
total := 0.0
|
total := 0.0
|
||||||
|
|||||||
@@ -141,18 +141,14 @@ 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 = "/uploads/image/t300/" + u + ".jpg"
|
||||||
it.FullURL = "/uploads/image/" + u + ".jpg"
|
it.FullURL = "/uploads/image/" + u + ".jpg"
|
||||||
}
|
}
|
||||||
if it.StoredInDB {
|
|
||||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resolved, _ := resolveStoragePath(it.Storage); resolved != "" {
|
|
||||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/product-images/batch
|
// POST /api/product-images/batch
|
||||||
@@ -576,14 +572,25 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
raw = filepath.ToSlash(filepath.Clean(raw))
|
raw = filepath.ToSlash(filepath.Clean(raw))
|
||||||
|
|
||||||
relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
|
relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
|
||||||
|
baseName := filepath.Base(raw)
|
||||||
|
relImage := filepath.FromSlash(filepath.Join("uploads", "image", baseName))
|
||||||
|
relImageThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", baseName))
|
||||||
candidates := []string{
|
candidates := []string{
|
||||||
filepath.Clean(storagePath),
|
filepath.Clean(storagePath),
|
||||||
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
|
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
|
||||||
filepath.FromSlash(filepath.Clean(raw)),
|
filepath.FromSlash(filepath.Clean(raw)),
|
||||||
relUploads,
|
relUploads,
|
||||||
|
relImage,
|
||||||
|
relImageThumb,
|
||||||
filepath.Join(".", relUploads),
|
filepath.Join(".", relUploads),
|
||||||
|
filepath.Join(".", relImage),
|
||||||
|
filepath.Join(".", relImageThumb),
|
||||||
filepath.Join("..", relUploads),
|
filepath.Join("..", relUploads),
|
||||||
|
filepath.Join("..", relImage),
|
||||||
|
filepath.Join("..", relImageThumb),
|
||||||
filepath.Join("..", "..", relUploads),
|
filepath.Join("..", "..", relUploads),
|
||||||
|
filepath.Join("..", "..", relImage),
|
||||||
|
filepath.Join("..", "..", relImageThumb),
|
||||||
}
|
}
|
||||||
|
|
||||||
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
|
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
|
||||||
@@ -591,6 +598,8 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
filepath.Join(root, raw),
|
filepath.Join(root, raw),
|
||||||
filepath.Join(root, relUploads),
|
filepath.Join(root, relUploads),
|
||||||
filepath.Join(root, "uploads", raw),
|
filepath.Join(root, "uploads", raw),
|
||||||
|
filepath.Join(root, relImage),
|
||||||
|
filepath.Join(root, relImageThumb),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,14 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
||||||
limit := intQuery(r, "limit", 50000)
|
limit := intQuery(r, "limit", 50000)
|
||||||
expanded := map[string]bool{}
|
expanded := map[string]bool{}
|
||||||
const maxExpandedGroupKeys = 260
|
filters := map[string][]string{}
|
||||||
|
expandThroughLevel := -1
|
||||||
|
if rawExpandLevel := strings.TrimSpace(r.URL.Query().Get("expand_through_level")); rawExpandLevel != "" {
|
||||||
|
if parsed, err := strconv.Atoi(rawExpandLevel); err == nil {
|
||||||
|
expandThroughLevel = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const maxExpandedGroupKeys = 1000
|
||||||
addExpandedKey := func(key string) {
|
addExpandedKey := func(key string) {
|
||||||
key = strings.TrimSpace(key)
|
key = strings.TrimSpace(key)
|
||||||
if key != "" && len(expanded) < maxExpandedGroupKeys {
|
if key != "" && len(expanded) < maxExpandedGroupKeys {
|
||||||
@@ -282,11 +289,13 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
var body struct {
|
var body struct {
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
GroupLevels []string `json:"group_levels"`
|
GroupLevels []string `json:"group_levels"`
|
||||||
ExpandedKeys []string `json:"expanded_keys"`
|
ExpandedKeys []string `json:"expanded_keys"`
|
||||||
Limit int `json:"limit"`
|
ExpandThroughLevel *int `json:"expand_through_level"`
|
||||||
MainGroup string `json:"urun_ana_grubu"`
|
Limit int `json:"limit"`
|
||||||
|
MainGroup string `json:"urun_ana_grubu"`
|
||||||
|
Filters map[string][]string `json:"filters"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
|
||||||
@@ -307,6 +316,12 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
for _, key := range body.ExpandedKeys {
|
for _, key := range body.ExpandedKeys {
|
||||||
addExpandedKey(key)
|
addExpandedKey(key)
|
||||||
}
|
}
|
||||||
|
if body.ExpandThroughLevel != nil {
|
||||||
|
expandThroughLevel = *body.ExpandThroughLevel
|
||||||
|
}
|
||||||
|
if len(body.Filters) > 0 {
|
||||||
|
filters = body.Filters
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, key := range strings.Split(r.URL.Query().Get("expanded_keys"), ",") {
|
for _, key := range strings.Split(r.URL.Query().Get("expanded_keys"), ",") {
|
||||||
addExpandedKey(key)
|
addExpandedKey(key)
|
||||||
@@ -314,11 +329,13 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := queries.ListProductPerformanceGrouped(ctx, pg, queries.ProductPerformanceGroupedRequest{
|
rows, err := queries.ListProductPerformanceGrouped(ctx, pg, queries.ProductPerformanceGroupedRequest{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
GroupLevels: groupLevels,
|
GroupLevels: groupLevels,
|
||||||
ExpandedKeys: expanded,
|
ExpandedKeys: expanded,
|
||||||
Limit: limit,
|
ExpandThroughLevel: expandThroughLevel,
|
||||||
MainGroup: mainGroup,
|
Limit: limit,
|
||||||
|
MainGroup: mainGroup,
|
||||||
|
Filters: filters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-7 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-7 bg-white"
|
||||||
:rows="filteredGeneralRows"
|
:rows="filteredGeneralRows"
|
||||||
:columns="generalColumns"
|
:columns="generalColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.general"
|
v-model:pagination="tablePagination.general"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-8 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-8 bg-white"
|
||||||
:rows="displayOrderProductCustomerTableRows"
|
:rows="displayOrderProductCustomerTableRows"
|
||||||
:columns="orderProductCustomerColumns"
|
:columns="orderProductCustomerColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.order_product_customers"
|
v-model:pagination="tablePagination.order_product_customers"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
|
||||||
:rows="displayOrderMarketDetailTableRows"
|
:rows="displayOrderMarketDetailTableRows"
|
||||||
:columns="orderMarketDetailColumns"
|
:columns="orderMarketDetailColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.order_market_details"
|
v-model:pagination="tablePagination.order_market_details"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
]"
|
]"
|
||||||
:rows="displayProductKpiTableRows"
|
:rows="displayProductKpiTableRows"
|
||||||
:columns="activeProductColumns"
|
:columns="activeProductColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="activeProductPagination"
|
v-model:pagination="activeProductPagination"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -717,7 +717,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-11 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-11 bg-white"
|
||||||
:rows="displayIdleTableRows"
|
:rows="displayIdleTableRows"
|
||||||
:columns="idleColumns"
|
:columns="idleColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.idle"
|
v-model:pagination="tablePagination.idle"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -904,7 +904,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-9 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-9 bg-white"
|
||||||
:rows="filteredMarketRows"
|
:rows="filteredMarketRows"
|
||||||
:columns="marketColumns"
|
:columns="marketColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.markets"
|
v-model:pagination="tablePagination.markets"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
]"
|
]"
|
||||||
:rows="displayActiveSalesBreakdownTableRows"
|
:rows="displayActiveSalesBreakdownTableRows"
|
||||||
:columns="visibleSalesBreakdownColumns"
|
:columns="visibleSalesBreakdownColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="activeSalesBreakdownPagination"
|
v-model:pagination="activeSalesBreakdownPagination"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -1170,7 +1170,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-5 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-5 bg-white"
|
||||||
:rows="filteredCustomerRows"
|
:rows="filteredCustomerRows"
|
||||||
:columns="customerColumns"
|
:columns="customerColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.customers"
|
v-model:pagination="tablePagination.customers"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -1272,7 +1272,7 @@
|
|||||||
class="performance-table sticky-dim-table sticky-dim-3 bg-white"
|
class="performance-table sticky-dim-table sticky-dim-3 bg-white"
|
||||||
:rows="filteredCountryRows"
|
:rows="filteredCountryRows"
|
||||||
:columns="countryColumns"
|
:columns="countryColumns"
|
||||||
:loading="loading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.countries"
|
v-model:pagination="tablePagination.countries"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:virtual-scroll-item-size="64"
|
:virtual-scroll-item-size="64"
|
||||||
@@ -1412,7 +1412,7 @@
|
|||||||
:name="idx"
|
:name="idx"
|
||||||
class="column no-wrap flex-center"
|
class="column no-wrap flex-center"
|
||||||
>
|
>
|
||||||
<q-img :src="url" fit="contain" class="product-image-large" />
|
<q-img :src="url" fit="contain" class="product-image-large" @error="markDialogImageFailed(url)" />
|
||||||
</q-carousel-slide>
|
</q-carousel-slide>
|
||||||
</q-carousel>
|
</q-carousel>
|
||||||
<div v-else class="product-image-empty">
|
<div v-else class="product-image-empty">
|
||||||
@@ -1522,7 +1522,8 @@ const performanceStore = useProductPerformanceStore()
|
|||||||
|
|
||||||
function formatNumber (value, fraction = 2) {
|
function formatNumber (value, fraction = 2) {
|
||||||
const n = Number(value || 0)
|
const n = Number(value || 0)
|
||||||
const digits = Math.max(0, Math.min(2, Number(fraction || 0)))
|
const requested = Number(fraction)
|
||||||
|
const digits = Number.isFinite(requested) ? Math.max(0, Math.min(4, requested)) : 2
|
||||||
return n.toLocaleString('tr-TR', {
|
return n.toLocaleString('tr-TR', {
|
||||||
minimumFractionDigits: digits,
|
minimumFractionDigits: digits,
|
||||||
maximumFractionDigits: digits
|
maximumFractionDigits: digits
|
||||||
@@ -1539,6 +1540,7 @@ function formatPercent (value) {
|
|||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
|
const detailMainGroupOptionRows = ref([])
|
||||||
const generalRows = ref([])
|
const generalRows = ref([])
|
||||||
const orderAnalysisRows = ref([])
|
const orderAnalysisRows = ref([])
|
||||||
const orderMarketRows = ref([])
|
const orderMarketRows = ref([])
|
||||||
@@ -1635,6 +1637,24 @@ const maxAutoExpandLevelByTab = {
|
|||||||
order_product_customers: 8,
|
order_product_customers: 8,
|
||||||
order_market_details: 8
|
order_market_details: 8
|
||||||
}
|
}
|
||||||
|
const backendGroupedFilterFields = new Set([
|
||||||
|
'kategori',
|
||||||
|
'askili_yan',
|
||||||
|
'urun_ilk_grubu',
|
||||||
|
'urun_ana_grubu',
|
||||||
|
'urun_alt_grubu',
|
||||||
|
'product_code',
|
||||||
|
'item_description',
|
||||||
|
'color_code',
|
||||||
|
'yaka_kodu',
|
||||||
|
'color_yaka',
|
||||||
|
'market_key',
|
||||||
|
'country',
|
||||||
|
'customer_segment',
|
||||||
|
'customer_code',
|
||||||
|
'customer_name',
|
||||||
|
'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' },
|
||||||
@@ -1646,6 +1666,7 @@ const performanceTabs = [
|
|||||||
{ name: 'order_market_details', icon: 'receipt_long', label: 'Piyasa > Müşteri > Ürün Sipariş' }
|
{ name: 'order_market_details', icon: 'receipt_long', label: 'Piyasa > Müşteri > Ürün Sipariş' }
|
||||||
]
|
]
|
||||||
const pageBusy = computed(() => loading.value)
|
const pageBusy = computed(() => loading.value)
|
||||||
|
const activeTableLoading = computed(() => loading.value || (backendGroupedSupportedTab(activeTab.value) && backendGroupedLoading.value))
|
||||||
const loadingElapsedLabel = computed(() => {
|
const loadingElapsedLabel = computed(() => {
|
||||||
if (!loadingStartedAt.value || !loadingNow.value) return ''
|
if (!loadingStartedAt.value || !loadingNow.value) return ''
|
||||||
const seconds = Math.max(0, Math.floor((loadingNow.value - loadingStartedAt.value) / 1000))
|
const seconds = Math.max(0, Math.floor((loadingNow.value - loadingStartedAt.value) / 1000))
|
||||||
@@ -1737,7 +1758,7 @@ const detailSalesColumns = [
|
|||||||
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
|
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
|
||||||
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
|
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
|
||||||
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
|
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
|
||||||
{ name: 'sales_qty', label: 'Adet', field: row => formatNumber(row.sales_qty, 0), align: 'right', sortable: true },
|
{ name: 'sales_qty', label: 'Adet', field: row => formatNumber(row.sales_qty, 2), align: 'right', sortable: true },
|
||||||
{ name: 'sales_usd', label: 'USD', field: row => formatMoney(row.sales_usd, 'USD'), align: 'right', sortable: true },
|
{ name: 'sales_usd', label: 'USD', field: row => formatMoney(row.sales_usd, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'avg_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd, 'USD'), align: 'right', sortable: true }
|
{ name: 'avg_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd, 'USD'), align: 'right', sortable: true }
|
||||||
]
|
]
|
||||||
@@ -2718,16 +2739,17 @@ const columnFilterOptionMap = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const productTableRows = computed(() => {
|
const productTableRows = computed(() => {
|
||||||
|
if (shouldUseBackendGroupedRows('products')) return []
|
||||||
const out = []
|
const out = []
|
||||||
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels, 'products')
|
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels, 'products')
|
||||||
return out
|
return out
|
||||||
})
|
})
|
||||||
const detailProductTableRows = computed(() => buildGroupedTableRows('product_detail', filteredDetailProductRows.value))
|
const detailProductTableRows = computed(() => shouldUseBackendGroupedRows('product_detail') ? [] : buildGroupedTableRows('product_detail', filteredDetailProductRows.value))
|
||||||
|
|
||||||
const idleTableRows = computed(() => buildGroupedTableRows('idle', filteredIdleRows.value))
|
const idleTableRows = computed(() => shouldUseBackendGroupedRows('idle') ? [] : buildGroupedTableRows('idle', filteredIdleRows.value))
|
||||||
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
const orderProductCustomerTableRows = computed(() => shouldUseBackendGroupedRows('order_product_customers') ? [] : buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
||||||
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
const orderMarketDetailTableRows = computed(() => shouldUseBackendGroupedRows('order_market_details') ? [] : buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
||||||
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
const activeSalesBreakdownTableRows = computed(() => shouldUseBackendGroupedRows(activeTab.value) ? [] : buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
||||||
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
||||||
const displayDetailProductTableRows = computed(() => backendRowsForTab('product_detail', detailProductTableRows.value))
|
const displayDetailProductTableRows = computed(() => backendRowsForTab('product_detail', detailProductTableRows.value))
|
||||||
const displayProductKpiTableRows = computed(() => activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : displayProductTableRows.value)
|
const displayProductKpiTableRows = computed(() => activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : displayProductTableRows.value)
|
||||||
@@ -2749,16 +2771,27 @@ const activeDisplayedTableRows = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const detailMainGroupOptions = computed(() => {
|
const detailMainGroupOptions = computed(() => {
|
||||||
|
if (detailMainGroupOptionRows.value.length) {
|
||||||
|
return [...detailMainGroupOptionRows.value]
|
||||||
|
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, 'tr'))
|
||||||
|
}
|
||||||
const grouped = new Map()
|
const grouped = new Map()
|
||||||
for (const row of rows.value) {
|
for (const row of rows.value) {
|
||||||
const value = productMainGroupValue(row)
|
const value = productMainGroupValue(row)
|
||||||
if (!value) continue
|
if (!value) continue
|
||||||
const current = grouped.get(value) || { value, label: value, stock_qty: 0, sales_qty_90d: 0 }
|
const current = grouped.get(value) || { value, label: value, stock_qty: 0, sales_qty_90d: 0, _stockSeen: new Set() }
|
||||||
current.stock_qty += Number(row?.stock_qty || 0)
|
const variantKey = productVariantMetricKey(row)
|
||||||
|
if (variantKey && !current._stockSeen.has(variantKey)) {
|
||||||
|
current._stockSeen.add(variantKey)
|
||||||
|
current.stock_qty += Number(row?.stock_qty || 0)
|
||||||
|
} else if (!variantKey) {
|
||||||
|
current.stock_qty += Number(row?.stock_qty || 0)
|
||||||
|
}
|
||||||
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
|
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
|
||||||
grouped.set(value, current)
|
grouped.set(value, current)
|
||||||
}
|
}
|
||||||
return Array.from(grouped.values())
|
return Array.from(grouped.values())
|
||||||
|
.map(({ _stockSeen, ...option }) => option)
|
||||||
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, 'tr'))
|
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, 'tr'))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2809,7 +2842,6 @@ function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLeve
|
|||||||
function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, levels = groupLevels, tableKey = activeTab.value) {
|
function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, levels = groupLevels, tableKey = activeTab.value) {
|
||||||
if (out.length >= maxRenderedGroupedRows) return
|
if (out.length >= maxRenderedGroupedRows) return
|
||||||
if (level >= levels.length) {
|
if (level >= levels.length) {
|
||||||
out.push(...sortLeafRowsForTable(tableKey, sourceRows).slice(0, Math.max(0, maxRenderedGroupedRows - out.length)))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2845,6 +2877,10 @@ function buildGroupedTableRows (tableKey, sourceRows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backendRowsForTab (tabKey, fallbackRows) {
|
function backendRowsForTab (tabKey, fallbackRows) {
|
||||||
|
if (shouldUseBackendGroupedRows(tabKey)) {
|
||||||
|
const rows = backendGroupedRows.value[tabKey]
|
||||||
|
return Array.isArray(rows) ? rows : []
|
||||||
|
}
|
||||||
return tabKey === 'product_detail'
|
return tabKey === 'product_detail'
|
||||||
? filterRowsBySelectedDetailMainGroup(fallbackRows)
|
? filterRowsBySelectedDetailMainGroup(fallbackRows)
|
||||||
: fallbackRows
|
: fallbackRows
|
||||||
@@ -2854,19 +2890,34 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
|
|||||||
return fallbackRows
|
return fallbackRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backendGroupedFilterState (tabKey) {
|
||||||
|
const out = {}
|
||||||
|
const columns = columnsForTableKey(tabKey)
|
||||||
|
for (const col of columns) {
|
||||||
|
const name = col?.name || ''
|
||||||
|
if (!isColumnFilterable(name)) continue
|
||||||
|
const selected = selectedColumnFilters(tabKey, name)
|
||||||
|
if (!selected.length) continue
|
||||||
|
if (!backendGroupedFilterFields.has(name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[name] = selected.map(value => String(value || '').trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
return { filters: out, unsupported: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseBackendGroupedRows (tabKey) {
|
||||||
|
return backendGroupedSupportedTab(tabKey) && !backendGroupedFilterState(tabKey).unsupported
|
||||||
|
}
|
||||||
|
|
||||||
function tableFilterSource (tableKey) {
|
function tableFilterSource (tableKey) {
|
||||||
if (tableKey === 'products') return { rows: filterSourceRowsForTab('products', rows.value), columns: productColumns.value }
|
if (backendGroupedSupportedTab(tableKey)) {
|
||||||
if (tableKey === 'product_detail') return { rows: filterSourceRowsForTab('product_detail', detailProductRows.value), columns: productDetailColumns.value }
|
return { rows: backendGroupedRows.value[tableKey] || [], columns: columnsForTableKey(tableKey) }
|
||||||
|
}
|
||||||
if (tableKey === 'general') return { rows: generalRows.value, columns: generalColumns }
|
if (tableKey === 'general') return { rows: generalRows.value, columns: generalColumns }
|
||||||
if (tableKey === 'order_product_customers') return { rows: filterSourceRowsForTab('order_product_customers', orderProductCustomerRows.value), columns: orderProductCustomerColumns }
|
|
||||||
if (tableKey === 'order_market_details') return { rows: filterSourceRowsForTab('order_market_details', orderMarketDetailRows.value), columns: orderMarketDetailColumns }
|
|
||||||
if (tableKey === 'idle') return { rows: filterSourceRowsForTab('idle', idleRows.value), columns: idleColumns }
|
|
||||||
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
|
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
|
||||||
if (tableKey === 'countries') return { rows: countryRows.value, columns: countryColumns }
|
if (tableKey === 'countries') return { rows: countryRows.value, columns: countryColumns }
|
||||||
if (tableKey === 'customers') return { rows: customerRows.value, columns: customerColumns }
|
if (tableKey === 'customers') return { rows: customerRows.value, columns: customerColumns }
|
||||||
if (salesBreakdownTabKeys.includes(tableKey)) {
|
|
||||||
return { rows: filterSourceRowsForTab(tableKey, salesBreakdownRows[tableKey] || []), columns: visibleSalesBreakdownColumns.value }
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3143,6 +3194,7 @@ function aggregateGroupFields (sourceRows, groupField = '') {
|
|||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (field === 'row_key' || field === 'key') continue
|
if (field === 'row_key' || field === 'key') continue
|
||||||
|
if (/^gross_margin(_base|_cost)?(_|$)/i.test(field)) continue
|
||||||
if (field === 'stock_qty') {
|
if (field === 'stock_qty') {
|
||||||
out[field] = distinctVariantStockQty(sourceRows)
|
out[field] = distinctVariantStockQty(sourceRows)
|
||||||
continue
|
continue
|
||||||
@@ -3546,6 +3598,7 @@ function applyActiveExpansionThroughLevel (level) {
|
|||||||
setSelectedExpandLevelsThrough(level)
|
setSelectedExpandLevelsThrough(level)
|
||||||
replaceActiveTabExpandedGroups([])
|
replaceActiveTabExpandedGroups([])
|
||||||
lastManualExpandedGroupKey.value = ''
|
lastManualExpandedGroupKey.value = ''
|
||||||
|
scheduleLoadBackendGroupedRows()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleGroup (key) {
|
function toggleGroup (key) {
|
||||||
@@ -3657,7 +3710,9 @@ function withProductMargins (row) {
|
|||||||
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_cost_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.cost_price_usd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3690,6 +3745,7 @@ function isColumnFilterable (name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function columnFilterOptions (tableKey, name) {
|
function columnFilterOptions (tableKey, name) {
|
||||||
|
if (backendGroupedSupportedTab(tableKey) && !backendGroupedFilterFields.has(name)) return []
|
||||||
const options = columnFilterOptionMap.value[tableKey]?.[name] || []
|
const options = columnFilterOptionMap.value[tableKey]?.[name] || []
|
||||||
const search = String(columnFilterSearch[filterKey(tableKey, name)] || '').trim().toLocaleLowerCase('tr')
|
const search = String(columnFilterSearch[filterKey(tableKey, name)] || '').trim().toLocaleLowerCase('tr')
|
||||||
if (!search) return options
|
if (!search) return options
|
||||||
@@ -3748,7 +3804,7 @@ function runWithFilterBusy (apply) {
|
|||||||
function syncExpansionAfterFilterChange () {
|
function syncExpansionAfterFilterChange () {
|
||||||
if (!backendGroupedSupportedTab(activeTab.value)) return
|
if (!backendGroupedSupportedTab(activeTab.value)) return
|
||||||
const level = selectedExpandThroughLevel.value
|
const level = selectedExpandThroughLevel.value
|
||||||
if (level >= 0) applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
|
applyActiveExpansionThroughLevel(level >= 0 ? autoExpandThroughLevel.value : -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
|
function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
|
||||||
@@ -3822,7 +3878,13 @@ function productFirstGroupValue (row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function productMainGroupValue (row) {
|
function productMainGroupValue (row) {
|
||||||
return row?.urun_ana_grubu || ''
|
const direct = String(row?.urun_ana_grubu || '').trim()
|
||||||
|
if (direct) return direct
|
||||||
|
if (row?.__group && row.group_field === 'urun_ana_grubu') {
|
||||||
|
const groupValue = String(row.group_value || row.label || '').trim()
|
||||||
|
if (groupValue) return groupValue
|
||||||
|
}
|
||||||
|
return String(row?.urun_alt_grubu || row?.kategori || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanOptionalGroupValue (value) {
|
function cleanOptionalGroupValue (value) {
|
||||||
@@ -4170,6 +4232,14 @@ function productImageKey (row) {
|
|||||||
return `${product}|${color}|${yaka}`
|
return `${product}|${color}|${yaka}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function productVariantMetricKey (row) {
|
||||||
|
const product = String(row?.image_product_code || row?.product_code || '').trim()
|
||||||
|
if (!product) return ''
|
||||||
|
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||||||
|
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
|
||||||
|
return `${product}|${color}|${yaka}`
|
||||||
|
}
|
||||||
|
|
||||||
function displayMarketName (value) {
|
function displayMarketName (value) {
|
||||||
const parts = String(value || '').split('|').map(part => part.trim()).filter(Boolean)
|
const parts = String(value || '').split('|').map(part => part.trim()).filter(Boolean)
|
||||||
return parts.length ? parts[parts.length - 1] : ''
|
return parts.length ? parts[parts.length - 1] : ''
|
||||||
@@ -4186,27 +4256,36 @@ function normalizeUploadsPath (storagePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveProductImageUrl (item) {
|
function resolveProductImageUrl (item) {
|
||||||
if (!item || typeof item !== 'object') return ''
|
return resolveProductImageUrls(item)[0] || ''
|
||||||
|
}
|
||||||
|
|
||||||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
function resolveProductImageUrls (item) {
|
||||||
if (thumbURL) return thumbURL
|
if (!item || typeof item !== 'object') return []
|
||||||
|
const urls = []
|
||||||
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
const addURL = value => {
|
||||||
if (fullURL) return fullURL
|
const url = String(value || '').trim()
|
||||||
|
if (url && !urls.includes(url)) urls.push(url)
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
}
|
||||||
if (uploadsPath) return uploadsPath
|
|
||||||
|
|
||||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
|
||||||
if (fileName) return `/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/')) return contentURL
|
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||||
|
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
const imageId = Number(item.id || item.ID || 0)
|
||||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
||||||
return ''
|
|
||||||
|
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||||
|
addURL(thumbURL)
|
||||||
|
|
||||||
|
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
||||||
|
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}`)
|
||||||
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProductImagesForRow (row) {
|
async function fetchProductImagesForRow (row) {
|
||||||
@@ -4239,7 +4318,7 @@ async function fetchProductImagesForRow (row) {
|
|||||||
timeout: 60000
|
timeout: 60000
|
||||||
})
|
})
|
||||||
const list = Array.isArray(resp?.data) ? resp.data : []
|
const list = Array.isArray(resp?.data) ? resp.data : []
|
||||||
const urls = list.map(resolveProductImageUrl).filter(Boolean)
|
const urls = list.flatMap(resolveProductImageUrls).filter(Boolean)
|
||||||
performanceStore.setImageCache(key, urls)
|
performanceStore.setImageCache(key, urls)
|
||||||
if (urls.length && imageFailedKeys.value.has(key)) {
|
if (urls.length && imageFailedKeys.value.has(key)) {
|
||||||
const nextFailed = new Set(imageFailedKeys.value)
|
const nextFailed = new Set(imageFailedKeys.value)
|
||||||
@@ -4278,10 +4357,10 @@ function queueVisibleProductImage (row) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function markProductImageFailed (row) {
|
function markProductImageFailed (row, failedUrl = '') {
|
||||||
const key = productImageKey(row)
|
const key = productImageKey(row)
|
||||||
if (!key) return
|
if (!key) return
|
||||||
const currentUrl = imageUrlByKey.value[key] || performanceStore.imageUrl(key) || ''
|
const currentUrl = String(failedUrl || imageUrlByKey.value[key] || performanceStore.imageUrl(key) || '').trim()
|
||||||
const list = imageListByKey.value[key] || performanceStore.imageList(key) || []
|
const list = imageListByKey.value[key] || performanceStore.imageList(key) || []
|
||||||
const nextList = Array.isArray(list) ? list.filter(url => url && url !== currentUrl) : []
|
const nextList = Array.isArray(list) ? list.filter(url => url && url !== currentUrl) : []
|
||||||
if (nextList.length) {
|
if (nextList.length) {
|
||||||
@@ -4296,6 +4375,14 @@ function markProductImageFailed (row) {
|
|||||||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
|
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markDialogImageFailed (url) {
|
||||||
|
const failedUrl = String(url || '').trim()
|
||||||
|
if (!failedUrl) return
|
||||||
|
imageDialogUrls.value = imageDialogUrls.value.filter(item => item && item !== failedUrl)
|
||||||
|
if (imageSlide.value >= imageDialogUrls.value.length) imageSlide.value = 0
|
||||||
|
if (imageDialogRow.value) markProductImageFailed(imageDialogRow.value, failedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
async function primeProductImages (sourceRows) {
|
async function primeProductImages (sourceRows) {
|
||||||
if (!isProductKpiTab.value) return
|
if (!isProductKpiTab.value) return
|
||||||
imagePrimeActiveRequests += 1
|
imagePrimeActiveRequests += 1
|
||||||
@@ -4515,6 +4602,11 @@ function switchPerformanceTab (tabName) {
|
|||||||
async function loadBackendGroupedRows (options = {}) {
|
async function loadBackendGroupedRows (options = {}) {
|
||||||
const tabKey = activeTab.value
|
const tabKey = activeTab.value
|
||||||
if (!backendGroupedSupportedTab(tabKey)) return
|
if (!backendGroupedSupportedTab(tabKey)) return
|
||||||
|
const filterState = backendGroupedFilterState(tabKey)
|
||||||
|
if (filterState.unsupported) {
|
||||||
|
backendGroupedLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
if (tabKey === 'product_detail' && !ensureDetailMainGroupSelection()) {
|
if (tabKey === 'product_detail' && !ensureDetailMainGroupSelection()) {
|
||||||
backendGroupedRows.value = {
|
backendGroupedRows.value = {
|
||||||
...backendGroupedRows.value,
|
...backendGroupedRows.value,
|
||||||
@@ -4525,12 +4617,16 @@ async function loadBackendGroupedRows (options = {}) {
|
|||||||
}
|
}
|
||||||
pruneActiveExpandedGroups()
|
pruneActiveExpandedGroups()
|
||||||
backendGroupedLoading.value = true
|
backendGroupedLoading.value = true
|
||||||
const requestKey = `${tabKey}|${activeExpandedGroupKeys().join('~')}|${selectedDetailMainGroup.value || ''}`
|
const filtersKey = JSON.stringify(filterState.filters)
|
||||||
|
const expandThroughLevel = selectedExpandThroughLevel.value
|
||||||
|
const requestKey = `${tabKey}|${expandThroughLevel}|${selectedDetailMainGroup.value || ''}|${filtersKey}`
|
||||||
const params = {
|
const params = {
|
||||||
mode: tabKey,
|
mode: tabKey,
|
||||||
groupLevels: activeGroupLevels.value.map(level => level.key),
|
groupLevels: activeGroupLevels.value.map(level => level.key),
|
||||||
expandedKeys: activeExpandedGroupKeys(),
|
expandedKeys: activeExpandedGroupKeys(),
|
||||||
limit: tabKey === 'products' || tabKey === 'product_detail' || tabKey === 'idle' ? productKpiFetchLimit : 50000
|
expandThroughLevel,
|
||||||
|
limit: tabKey === 'products' || tabKey === 'product_detail' || tabKey === 'idle' ? productKpiFetchLimit : 50000,
|
||||||
|
filters: filterState.filters
|
||||||
}
|
}
|
||||||
if (tabKey === 'product_detail') {
|
if (tabKey === 'product_detail') {
|
||||||
params.urunAnaGrubu = selectedDetailMainGroup.value
|
params.urunAnaGrubu = selectedDetailMainGroup.value
|
||||||
@@ -4538,7 +4634,8 @@ async function loadBackendGroupedRows (options = {}) {
|
|||||||
try {
|
try {
|
||||||
const startedAt = performance.now()
|
const startedAt = performance.now()
|
||||||
const rows = await performanceStore.fetchGroupedRows(params, { force: options.force === true })
|
const rows = await performanceStore.fetchGroupedRows(params, { force: options.force === true })
|
||||||
const currentKey = `${activeTab.value}|${activeExpandedGroupKeys().join('~')}|${selectedDetailMainGroup.value || ''}`
|
const currentFilterState = backendGroupedFilterState(activeTab.value)
|
||||||
|
const currentKey = `${activeTab.value}|${selectedExpandThroughLevel.value}|${selectedDetailMainGroup.value || ''}|${JSON.stringify(currentFilterState.filters)}`
|
||||||
if (requestKey !== currentKey) return
|
if (requestKey !== currentKey) return
|
||||||
const normalizedRows = Array.isArray(rows) ? rows.map(row => normalizeGroupedDisplayRow(tabKey, row)) : []
|
const normalizedRows = Array.isArray(rows) ? rows.map(row => normalizeGroupedDisplayRow(tabKey, row)) : []
|
||||||
backendGroupedRows.value = {
|
backendGroupedRows.value = {
|
||||||
@@ -4563,7 +4660,7 @@ function normalizeGroupedDisplayRow (tabKey, row) {
|
|||||||
if (tabKey === 'order_product_customers' || tabKey === 'order_market_details') {
|
if (tabKey === 'order_product_customers' || tabKey === 'order_market_details') {
|
||||||
return withOrderPeriodScores(nextRow)
|
return withOrderPeriodScores(nextRow)
|
||||||
}
|
}
|
||||||
const next = withPeriodScores(nextRow)
|
const next = withPeriodScores(withGeneralMargins(withProductMargins(nextRow)))
|
||||||
if (tabKey !== 'products' && tabKey !== 'product_detail' && tabKey !== 'idle') {
|
if (tabKey !== 'products' && tabKey !== 'product_detail' && tabKey !== 'idle') {
|
||||||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||||||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||||||
@@ -4620,6 +4717,8 @@ async function reload () {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
startLoadingTimer('Rapor verileri hazırlanıyor...')
|
startLoadingTimer('Rapor verileri hazırlanıyor...')
|
||||||
try {
|
try {
|
||||||
|
rows.value = []
|
||||||
|
detailMainGroupOptionRows.value = []
|
||||||
generalRowsLoaded.value = false
|
generalRowsLoaded.value = false
|
||||||
generalRows.value = []
|
generalRows.value = []
|
||||||
orderProductCustomersLoaded.value = false
|
orderProductCustomersLoaded.value = false
|
||||||
@@ -4639,22 +4738,15 @@ async function reload () {
|
|||||||
for (const key of salesBreakdownTabKeys) salesBreakdownRows[key] = []
|
for (const key of salesBreakdownTabKeys) salesBreakdownRows[key] = []
|
||||||
|
|
||||||
loadingStage.value = 'Ana KPI ve özet yükleniyor...'
|
loadingStage.value = 'Ana KPI ve özet yükleniyor...'
|
||||||
const [summaryResp, listResp] = await Promise.all([
|
const [summaryResp] = await Promise.all([
|
||||||
timedProductPerformanceGet('summary', '/pricing/product-performance/summary', { timeout: 180000 }),
|
timedProductPerformanceGet('summary', '/pricing/product-performance/summary', { timeout: 180000 }),
|
||||||
timedProductPerformanceGet('product-kpi', '/pricing/product-performance', {
|
loadDetailMainGroupOptions(true)
|
||||||
params: {
|
|
||||||
page: 1,
|
|
||||||
limit: productKpiFetchLimit
|
|
||||||
},
|
|
||||||
timeout: 180000
|
|
||||||
})
|
|
||||||
])
|
])
|
||||||
summary.value = summaryResp?.data || {}
|
summary.value = summaryResp?.data || {}
|
||||||
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
|
|
||||||
loadingStage.value = 'Tüm rapor sekmeleri cacheleniyor...'
|
|
||||||
await preloadAllReportTabs()
|
|
||||||
loadingStage.value = 'Rapor ekranı hazırlanıyor...'
|
loadingStage.value = 'Rapor ekranı hazırlanıyor...'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
ensureDetailMainGroupSelection()
|
||||||
|
applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
|
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -4663,6 +4755,28 @@ async function reload () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDetailMainGroupOptions (force = false) {
|
||||||
|
const optionRows = await performanceStore.fetchGroupedRows({
|
||||||
|
mode: 'products',
|
||||||
|
groupLevels: ['urun_ana_grubu'],
|
||||||
|
expandThroughLevel: -1,
|
||||||
|
limit: productKpiFetchLimit
|
||||||
|
}, { force })
|
||||||
|
detailMainGroupOptionRows.value = (Array.isArray(optionRows) ? optionRows : [])
|
||||||
|
.filter(row => row?.__group)
|
||||||
|
.map(row => {
|
||||||
|
const normalized = normalizeGroupedDisplayRow('products', row)
|
||||||
|
const value = productMainGroupValue(normalized) || String(normalized.group_value || normalized.label || '').trim()
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: value || '-',
|
||||||
|
stock_qty: Number(normalized.stock_qty || 0),
|
||||||
|
sales_qty_90d: Number(normalized.sales_qty_90d || 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(option => option.value)
|
||||||
|
}
|
||||||
|
|
||||||
function startLoadingTimer (stage = '') {
|
function startLoadingTimer (stage = '') {
|
||||||
loadingStage.value = stage
|
loadingStage.value = stage
|
||||||
loadingStartedAt.value = Date.now()
|
loadingStartedAt.value = Date.now()
|
||||||
@@ -4781,7 +4895,7 @@ async function loadMarketRows (showLoading = true) {
|
|||||||
if (showLoading) loading.value = true
|
if (showLoading) loading.value = true
|
||||||
try {
|
try {
|
||||||
const resp = await timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 180000 })
|
const resp = await timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 180000 })
|
||||||
marketRows.value = Array.isArray(resp?.data) ? resp.data : []
|
marketRows.value = aggregateMarketRowsByDisplayName(Array.isArray(resp?.data) ? resp.data : [])
|
||||||
marketRowsLoaded.value = true
|
marketRowsLoaded.value = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Piyasa performans verisi alınamadı' })
|
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Piyasa performans verisi alınamadı' })
|
||||||
@@ -4790,6 +4904,60 @@ async function loadMarketRows (showLoading = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aggregateMarketRowsByDisplayName (sourceRows) {
|
||||||
|
const grouped = new Map()
|
||||||
|
for (const row of sourceRows || []) {
|
||||||
|
const display = displayMarketName(row?.market_key) || String(row?.market_key || '').trim() || '-'
|
||||||
|
const current = grouped.get(display) || {
|
||||||
|
...row,
|
||||||
|
row_key: `market|${display}`,
|
||||||
|
market_key: display,
|
||||||
|
product_count: 0,
|
||||||
|
star_count: 0,
|
||||||
|
stock_risk_count: 0,
|
||||||
|
stock_qty: 0,
|
||||||
|
stock_cost_value_usd: 0,
|
||||||
|
risk_stock_cost_value_usd: 0,
|
||||||
|
sales_qty_90d: 0,
|
||||||
|
sales_usd_90d: 0,
|
||||||
|
gross_profit_usd_90d: 0,
|
||||||
|
_margin_weight: 0,
|
||||||
|
_margin_total: 0,
|
||||||
|
_stock_days_weight: 0,
|
||||||
|
_stock_days_total: 0
|
||||||
|
}
|
||||||
|
current.product_count += Number(row?.product_count || 0)
|
||||||
|
current.star_count += Number(row?.star_count || 0)
|
||||||
|
current.stock_risk_count += Number(row?.stock_risk_count || 0)
|
||||||
|
current.stock_qty += Number(row?.stock_qty || 0)
|
||||||
|
current.stock_cost_value_usd += Number(row?.stock_cost_value_usd || 0)
|
||||||
|
current.risk_stock_cost_value_usd += Number(row?.risk_stock_cost_value_usd || 0)
|
||||||
|
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
|
||||||
|
current.sales_usd_90d += Number(row?.sales_usd_90d || 0)
|
||||||
|
current.gross_profit_usd_90d += Number(row?.gross_profit_usd_90d || 0)
|
||||||
|
const marginWeight = Math.abs(Number(row?.sales_usd_90d || 0))
|
||||||
|
current._margin_weight += marginWeight
|
||||||
|
current._margin_total += Number(row?.avg_gross_margin_90d || 0) * marginWeight
|
||||||
|
const stockDaysWeight = Math.abs(Number(row?.stock_qty || 0))
|
||||||
|
current._stock_days_weight += stockDaysWeight
|
||||||
|
current._stock_days_total += Number(row?.avg_stock_days_90d || 0) * stockDaysWeight
|
||||||
|
grouped.set(display, current)
|
||||||
|
}
|
||||||
|
return Array.from(grouped.values())
|
||||||
|
.map(row => {
|
||||||
|
row.avg_gross_margin_90d = row._margin_weight > 0 ? row._margin_total / row._margin_weight : 0
|
||||||
|
row.avg_stock_days_90d = row._stock_days_weight > 0 ? row._stock_days_total / row._stock_days_weight : 0
|
||||||
|
delete row._margin_weight
|
||||||
|
delete row._margin_total
|
||||||
|
delete row._stock_days_weight
|
||||||
|
delete row._stock_days_total
|
||||||
|
return row
|
||||||
|
})
|
||||||
|
.sort((a, b) => Number(b.risk_stock_cost_value_usd || 0) - Number(a.risk_stock_cost_value_usd || 0) ||
|
||||||
|
Number(b.stock_cost_value_usd || 0) - Number(a.stock_cost_value_usd || 0) ||
|
||||||
|
Number(b.sales_usd_90d || 0) - Number(a.sales_usd_90d || 0))
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCountryRows (showLoading = true) {
|
async function loadCountryRows (showLoading = true) {
|
||||||
if (countryRowsLoaded.value) return
|
if (countryRowsLoaded.value) return
|
||||||
if (showLoading) loading.value = true
|
if (showLoading) loading.value = true
|
||||||
@@ -4833,9 +5001,9 @@ async function ensureActiveTabData (showLoading = true) {
|
|||||||
if (tab === 'markets') return loadMarketRows(showLoading)
|
if (tab === 'markets') return loadMarketRows(showLoading)
|
||||||
if (tab === 'countries') return loadCountryRows(showLoading)
|
if (tab === 'countries') return loadCountryRows(showLoading)
|
||||||
if (tab === 'customers') return loadCustomerRows(showLoading)
|
if (tab === 'customers') return loadCustomerRows(showLoading)
|
||||||
if (tab === 'order_product_customers') return loadOrderProductCustomers(showLoading)
|
if (backendGroupedSupportedTab(tab)) {
|
||||||
if (tab === 'order_market_details') return loadOrderMarketDetails(showLoading)
|
scheduleLoadBackendGroupedRows()
|
||||||
if (salesBreakdownTabKeys.includes(tab)) return loadSalesBreakdownRows(tab, showLoading)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOrderAnalysis (showLoading = true) {
|
async function loadOrderAnalysis (showLoading = true) {
|
||||||
@@ -4874,7 +5042,9 @@ function bucketColor (value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(activeTab, () => {
|
watch(activeTab, () => {
|
||||||
void ensureActiveTabData(false)
|
void ensureActiveTabData(false).then(() => {
|
||||||
|
if (backendGroupedSupportedTab(activeTab.value)) applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
|
||||||
|
})
|
||||||
setupTopScrollbarSync()
|
setupTopScrollbarSync()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -5263,95 +5433,95 @@ onBeforeUnmount(() => {
|
|||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(-n+10)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(-n+10)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(-n+10)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(-n+10)) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(-n+10)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(-n+10)) {
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(1)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(1)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(1)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(1)) {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 190px;
|
width: 190px;
|
||||||
min-width: 190px;
|
min-width: 190px;
|
||||||
max-width: 190px;
|
max-width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(2)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(2)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(2)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(2)) {
|
||||||
left: 190px;
|
left: 190px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
max-width: 140px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(3)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(3)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(3)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(3)) {
|
||||||
left: 330px;
|
left: 330px;
|
||||||
width: 110px;
|
width: 110px;
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
max-width: 110px;
|
max-width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(4)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(4)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(4)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(4)) {
|
||||||
left: 440px;
|
left: 440px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(5)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(5)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(5)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(5)) {
|
||||||
left: 590px;
|
left: 590px;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
max-width: 170px;
|
max-width: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(6)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(6)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(6)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(6)) {
|
||||||
left: 760px;
|
left: 760px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(7)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(7)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(7)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(7)) {
|
||||||
left: 910px;
|
left: 910px;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
max-width: 130px;
|
max-width: 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(8)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(8)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(8)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(8)) {
|
||||||
left: 1040px;
|
left: 1040px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
max-width: 90px;
|
max-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(9)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(9)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(9)) {
|
||||||
left: 1130px;
|
left: 1130px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(10)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(10)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(10)) {
|
||||||
left: 1210px;
|
left: 1210px;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
@@ -5475,8 +5645,8 @@ onBeforeUnmount(() => {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
||||||
.product-breakdown-table :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
|
||||||
|
|||||||
@@ -15,27 +15,46 @@ function normalizeUploadsPath (storagePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveProductImageUrl (item) {
|
function resolveProductImageUrl (item) {
|
||||||
if (!item || typeof item !== 'object') return ''
|
return resolveProductImageUrls(item)[0] || ''
|
||||||
|
}
|
||||||
|
|
||||||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
function resolveProductImageUrls (item) {
|
||||||
if (thumbURL) return thumbURL
|
if (!item || typeof item !== 'object') return []
|
||||||
|
const urls = []
|
||||||
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
const addURL = value => {
|
||||||
if (fullURL) return fullURL
|
const url = String(value || '').trim()
|
||||||
|
if (url && !urls.includes(url)) urls.push(url)
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
}
|
||||||
if (uploadsPath) return uploadsPath
|
|
||||||
|
|
||||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
|
||||||
if (fileName) return `/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/')) return contentURL
|
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||||
|
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
const imageId = Number(item.id || item.ID || 0)
|
||||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
||||||
return ''
|
|
||||||
|
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||||
|
addURL(thumbURL)
|
||||||
|
|
||||||
|
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
||||||
|
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}`)
|
||||||
|
return urls
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedFilterMap (filters = {}) {
|
||||||
|
return Object.fromEntries(Object.entries(filters || {})
|
||||||
|
.map(([key, values]) => [
|
||||||
|
String(key || '').trim(),
|
||||||
|
normalizedList(values, true)
|
||||||
|
])
|
||||||
|
.filter(([key, values]) => key && values.length)
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0], 'tr')))
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizedList (value, sort = false) {
|
function normalizedList (value, sort = false) {
|
||||||
@@ -69,6 +88,8 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
|
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
|
||||||
groupLevels: normalizedList(params.groupLevels),
|
groupLevels: normalizedList(params.groupLevels),
|
||||||
expandedKeys: normalizedList(params.expandedKeys, true),
|
expandedKeys: normalizedList(params.expandedKeys, true),
|
||||||
|
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
|
||||||
|
filters: normalizedFilterMap(params.filters),
|
||||||
limit: Number(params.limit || 0)
|
limit: Number(params.limit || 0)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -91,7 +112,8 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
console.info(`${logPrefix} in-flight reuse`, {
|
console.info(`${logPrefix} in-flight reuse`, {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
groupLevels: normalizedList(params.groupLevels),
|
groupLevels: normalizedList(params.groupLevels),
|
||||||
expandedKeys: normalizedList(params.expandedKeys, true).length
|
expandedKeys: normalizedList(params.expandedKeys, true).length,
|
||||||
|
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1)
|
||||||
})
|
})
|
||||||
return this.groupedInFlightByKey[key]
|
return this.groupedInFlightByKey[key]
|
||||||
}
|
}
|
||||||
@@ -102,6 +124,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
groupLevels: normalizedList(params.groupLevels),
|
groupLevels: normalizedList(params.groupLevels),
|
||||||
expandedKeys: normalizedList(params.expandedKeys, true).length,
|
expandedKeys: normalizedList(params.expandedKeys, true).length,
|
||||||
|
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
|
||||||
limit: params.limit
|
limit: params.limit
|
||||||
})
|
})
|
||||||
const request = api.post('/pricing/product-performance/grouped', {
|
const request = api.post('/pricing/product-performance/grouped', {
|
||||||
@@ -109,12 +132,15 @@ 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),
|
||||||
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
||||||
|
expand_through_level: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
|
||||||
|
filters: normalizedFilterMap(params.filters),
|
||||||
limit: params.limit
|
limit: params.limit
|
||||||
}, {
|
}, {
|
||||||
params: {
|
params: {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || ''
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
||||||
|
expand_through_level: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1)
|
||||||
},
|
},
|
||||||
timeout: 90000
|
timeout: 90000
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
@@ -190,7 +216,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
const key = String(item?.key || '').trim()
|
const key = String(item?.key || '').trim()
|
||||||
if (!key) continue
|
if (!key) continue
|
||||||
returned.add(key)
|
returned.add(key)
|
||||||
const urls = (Array.isArray(item.images) ? item.images : []).map(resolveProductImageUrl).filter(Boolean)
|
const urls = (Array.isArray(item.images) ? item.images : []).flatMap(resolveProductImageUrls).filter(Boolean)
|
||||||
this.setImageCache(key, urls)
|
this.setImageCache(key, urls)
|
||||||
lists[key] = urls
|
lists[key] = urls
|
||||||
urlsByKey[key] = urls[0] || ''
|
urlsByKey[key] = urls[0] || ''
|
||||||
|
|||||||
Reference in New Issue
Block a user