|
|
|
@@ -10,6 +10,7 @@ import (
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
@@ -581,6 +582,35 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot_meta (
|
|
|
|
|
row_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
|
duration_ms BIGINT NOT NULL DEFAULT 0
|
|
|
|
|
)`,
|
|
|
|
|
`
|
|
|
|
|
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot (
|
|
|
|
|
report_key TEXT NOT NULL,
|
|
|
|
|
row_order INTEGER NOT NULL,
|
|
|
|
|
mode TEXT NOT NULL,
|
|
|
|
|
group_levels_key TEXT NOT NULL,
|
|
|
|
|
main_group TEXT NOT NULL DEFAULT '',
|
|
|
|
|
group_level INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
group_key TEXT NOT NULL DEFAULT '',
|
|
|
|
|
parent_key TEXT NOT NULL DEFAULT '',
|
|
|
|
|
group_field TEXT NOT NULL DEFAULT '',
|
|
|
|
|
group_value TEXT NOT NULL DEFAULT '',
|
|
|
|
|
payload JSONB NOT NULL,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
|
CONSTRAINT pk_mk_product_performance_grouped_snapshot PRIMARY KEY (report_key, row_order)
|
|
|
|
|
)`,
|
|
|
|
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_key ON mk_product_performance_grouped_snapshot (report_key, row_order)`,
|
|
|
|
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_level ON mk_product_performance_grouped_snapshot (report_key, group_level, row_order)`,
|
|
|
|
|
`
|
|
|
|
|
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot_meta (
|
|
|
|
|
report_key TEXT PRIMARY KEY,
|
|
|
|
|
mode TEXT NOT NULL,
|
|
|
|
|
group_levels_key TEXT NOT NULL,
|
|
|
|
|
main_group TEXT NOT NULL DEFAULT '',
|
|
|
|
|
row_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
|
duration_ms BIGINT NOT NULL DEFAULT 0
|
|
|
|
|
)`,
|
|
|
|
|
}
|
|
|
|
|
for _, stmt := range stmts {
|
|
|
|
@@ -669,6 +699,62 @@ SELECT EXISTS (
|
|
|
|
|
return out, exists, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadProductPerformanceSnapshotMapRows(ctx context.Context, pg *sql.DB, reportKey string, limit int) ([]map[string]any, bool, error) {
|
|
|
|
|
if !productPerformanceUseSnapshot(ctx) || pg == nil || strings.TrimSpace(reportKey) == "" {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
if limit <= 0 || limit > 50000 {
|
|
|
|
|
limit = 50000
|
|
|
|
|
}
|
|
|
|
|
rows, err := pg.QueryContext(ctx, `
|
|
|
|
|
SELECT payload
|
|
|
|
|
FROM mk_product_performance_report_snapshot
|
|
|
|
|
WHERE report_key = $1
|
|
|
|
|
ORDER BY row_order
|
|
|
|
|
LIMIT $2
|
|
|
|
|
`, reportKey, limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
out := make([]map[string]any, 0, limit)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var raw []byte
|
|
|
|
|
if err := rows.Scan(&raw); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
var item map[string]any
|
|
|
|
|
if err := json.Unmarshal(raw, &item); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
if item == nil {
|
|
|
|
|
item = map[string]any{}
|
|
|
|
|
}
|
|
|
|
|
out = append(out, item)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
if len(out) > 0 {
|
|
|
|
|
return out, true, nil
|
|
|
|
|
}
|
|
|
|
|
var exists bool
|
|
|
|
|
if err := pg.QueryRowContext(ctx, `
|
|
|
|
|
SELECT EXISTS (
|
|
|
|
|
SELECT 1
|
|
|
|
|
FROM mk_product_performance_report_snapshot_meta
|
|
|
|
|
WHERE report_key = $1
|
|
|
|
|
)
|
|
|
|
|
`, reportKey).Scan(&exists); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
if !exists && !productPerformanceLiveFallbackEnabled() {
|
|
|
|
|
return out, true, nil
|
|
|
|
|
}
|
|
|
|
|
return out, exists, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadProductPerformanceSnapshotItem[T any](ctx context.Context, pg *sql.DB, reportKey string) (T, bool, error) {
|
|
|
|
|
var zero T
|
|
|
|
|
rows, ok, err := loadProductPerformanceSnapshotRows[T](ctx, pg, reportKey, 1)
|
|
|
|
@@ -721,10 +807,78 @@ ON CONFLICT (report_key) DO UPDATE SET
|
|
|
|
|
return tx.Commit()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveProductPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, reportKey string, def productPerformanceGroupedSnapshotDefinition, rows []map[string]any, started time.Time) error {
|
|
|
|
|
if pg == nil || strings.TrimSpace(reportKey) == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback()
|
|
|
|
|
|
|
|
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key = $1`, reportKey); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
stmt, err := tx.PrepareContext(ctx, `
|
|
|
|
|
INSERT INTO mk_product_performance_grouped_snapshot (
|
|
|
|
|
report_key, row_order, mode, group_levels_key, main_group,
|
|
|
|
|
group_level, group_key, parent_key, group_field, group_value, payload, updated_at
|
|
|
|
|
)
|
|
|
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now())
|
|
|
|
|
`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer stmt.Close()
|
|
|
|
|
|
|
|
|
|
levelsKey := productPerformanceGroupedLevelsKey(def.Levels)
|
|
|
|
|
for i, row := range rows {
|
|
|
|
|
raw, err := json.Marshal(row)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
key := stringFromMap(row, "key")
|
|
|
|
|
if _, err := stmt.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
reportKey,
|
|
|
|
|
i,
|
|
|
|
|
strings.TrimSpace(def.Mode),
|
|
|
|
|
levelsKey,
|
|
|
|
|
strings.TrimSpace(def.MainGroup),
|
|
|
|
|
intFromMap(row, "level"),
|
|
|
|
|
key,
|
|
|
|
|
productPerformanceParentGroupKey(key),
|
|
|
|
|
stringFromMap(row, "group_field"),
|
|
|
|
|
stringFromMap(row, "group_value"),
|
|
|
|
|
raw,
|
|
|
|
|
); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if _, err := tx.ExecContext(ctx, `
|
|
|
|
|
INSERT INTO mk_product_performance_grouped_snapshot_meta (
|
|
|
|
|
report_key, mode, group_levels_key, main_group, row_count, refreshed_at, duration_ms
|
|
|
|
|
)
|
|
|
|
|
VALUES ($1,$2,$3,$4,$5,now(),$6)
|
|
|
|
|
ON CONFLICT (report_key) DO UPDATE SET
|
|
|
|
|
mode = EXCLUDED.mode,
|
|
|
|
|
group_levels_key = EXCLUDED.group_levels_key,
|
|
|
|
|
main_group = EXCLUDED.main_group,
|
|
|
|
|
row_count = EXCLUDED.row_count,
|
|
|
|
|
refreshed_at = EXCLUDED.refreshed_at,
|
|
|
|
|
duration_ms = EXCLUDED.duration_ms
|
|
|
|
|
`, reportKey, strings.TrimSpace(def.Mode), levelsKey, strings.TrimSpace(def.MainGroup), len(rows), time.Since(started).Milliseconds()); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tx.Commit()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (int, error) {
|
|
|
|
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
snapshotReadCtx := ctx
|
|
|
|
|
ctx = productPerformanceSnapshotBypass(ctx)
|
|
|
|
|
total := 0
|
|
|
|
|
|
|
|
|
@@ -846,9 +1000,97 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
groupedRows, err := RebuildProductPerformanceGroupedSnapshots(snapshotReadCtx, pg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
total += groupedRows
|
|
|
|
|
return total, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type productPerformanceGroupedSnapshotDefinition struct {
|
|
|
|
|
Mode string
|
|
|
|
|
Levels []string
|
|
|
|
|
MainGroup string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB) (int, error) {
|
|
|
|
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
defs, err := productPerformanceGroupedSnapshotDefinitions(ctx, pg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
total := 0
|
|
|
|
|
for _, def := range defs {
|
|
|
|
|
started := time.Now()
|
|
|
|
|
levels := sanitizeProductPerformanceGroupLevels(def.Levels)
|
|
|
|
|
if len(levels) == 0 {
|
|
|
|
|
levels = defaultProductPerformanceGroupLevels(def.Mode)
|
|
|
|
|
}
|
|
|
|
|
sourceRows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, def.Mode, 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
req := ProductPerformanceGroupedRequest{
|
|
|
|
|
Mode: def.Mode,
|
|
|
|
|
MainGroup: def.MainGroup,
|
|
|
|
|
}
|
|
|
|
|
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
|
|
|
|
|
rows := make([]map[string]any, 0, len(sourceRows))
|
|
|
|
|
appendProductPerformanceGroupedRows(&rows, sourceRows, levels, 0, 0, []string{"tab:" + def.Mode}, nil, len(levels)-1)
|
|
|
|
|
reportKey := productPerformanceGroupedSnapshotReportKey(def.Mode, levels, def.MainGroup)
|
|
|
|
|
if err := saveProductPerformanceGroupedSnapshotRows(ctx, pg, reportKey, productPerformanceGroupedSnapshotDefinition{
|
|
|
|
|
Mode: def.Mode,
|
|
|
|
|
Levels: levels,
|
|
|
|
|
MainGroup: def.MainGroup,
|
|
|
|
|
}, rows, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
total += len(rows)
|
|
|
|
|
}
|
|
|
|
|
return total, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
|
|
|
|
|
defs := []productPerformanceGroupedSnapshotDefinition{
|
|
|
|
|
{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
|
|
|
|
|
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
|
|
|
|
|
{Mode: "idle", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
|
|
|
|
{Mode: "sales_color_yaka_market_customer", Levels: []string{"color_yaka", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "country", "market_key", "customer_segment", "customer_code", "customer_name"}},
|
|
|
|
|
{Mode: "sales_product_country_segment_market_customer", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}},
|
|
|
|
|
{Mode: "sales_country_segment_market_customer_product", Levels: []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
|
|
|
|
{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
|
|
|
|
|
{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
|
|
|
|
}
|
|
|
|
|
productRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("products"), 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if ok {
|
|
|
|
|
mainGroups := make([]string, 0)
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
for _, row := range productRows {
|
|
|
|
|
mainGroup := productPerformanceGroupedFilterValue(row, "urun_ana_grubu")
|
|
|
|
|
if mainGroup == "" || seen[mainGroup] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[mainGroup] = true
|
|
|
|
|
mainGroups = append(mainGroups, mainGroup)
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(mainGroups)
|
|
|
|
|
for _, mainGroup := range mainGroups {
|
|
|
|
|
defs = append(defs, productPerformanceGroupedSnapshotDefinition{
|
|
|
|
|
Mode: "product_detail",
|
|
|
|
|
Levels: []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"},
|
|
|
|
|
MainGroup: mainGroup,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return defs, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
|
|
|
|
|
started := time.Now()
|
|
|
|
|
stage := productPerformanceRefreshStage(req.Stage)
|
|
|
|
@@ -3472,9 +3714,14 @@ type ProductPerformanceGroupedRequest struct {
|
|
|
|
|
Limit int
|
|
|
|
|
MainGroup string
|
|
|
|
|
Filters map[string][]string
|
|
|
|
|
SortBy string
|
|
|
|
|
Descending bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
|
|
|
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if req.Limit <= 0 || req.Limit > 50000 {
|
|
|
|
|
req.Limit = 50000
|
|
|
|
|
}
|
|
|
|
@@ -3486,6 +3733,13 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|
|
|
|
req.ExpandThroughLevel = len(levels) - 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if out, ok, err := loadProductPerformancePreparedGroupedRows(ctx, pg, req, levels); ok || err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
if !productPerformanceLiveFallbackEnabled() {
|
|
|
|
|
return []map[string]any{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sourceRows, err := productPerformanceGroupedSourceRows(ctx, pg, req.Mode, req.Limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
@@ -3502,6 +3756,215 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
|
|
|
|
reportKey := productPerformanceGroupedSnapshotReportKey(req.Mode, levels, req.MainGroup)
|
|
|
|
|
if strings.TrimSpace(reportKey) == "" {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
effectiveFilters := productPerformanceGroupedEffectiveFilters(req)
|
|
|
|
|
hasFilters := len(effectiveFilters) > 0
|
|
|
|
|
hasManualExpansion := len(req.ExpandedKeys) > 0
|
|
|
|
|
query := `
|
|
|
|
|
SELECT payload
|
|
|
|
|
FROM mk_product_performance_grouped_snapshot
|
|
|
|
|
WHERE report_key = $1
|
|
|
|
|
`
|
|
|
|
|
args := []any{reportKey}
|
|
|
|
|
if !hasFilters && !hasManualExpansion {
|
|
|
|
|
maxVisibleLevel := req.ExpandThroughLevel + 1
|
|
|
|
|
if maxVisibleLevel < 0 {
|
|
|
|
|
maxVisibleLevel = 0
|
|
|
|
|
}
|
|
|
|
|
args = append(args, maxVisibleLevel)
|
|
|
|
|
query += fmt.Sprintf(" AND group_level <= $%d\n", len(args))
|
|
|
|
|
}
|
|
|
|
|
query += "ORDER BY row_order"
|
|
|
|
|
if !hasFilters && !hasManualExpansion {
|
|
|
|
|
args = append(args, req.Limit)
|
|
|
|
|
query += fmt.Sprintf("\nLIMIT $%d", len(args))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows, err := pg.QueryContext(ctx, query, args...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
out := make([]map[string]any, 0, minInt(req.Limit, 50000))
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var raw []byte
|
|
|
|
|
if err := rows.Scan(&raw); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
var item map[string]any
|
|
|
|
|
if err := json.Unmarshal(raw, &item); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
if item == nil {
|
|
|
|
|
item = map[string]any{}
|
|
|
|
|
}
|
|
|
|
|
out = append(out, item)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
if len(out) == 0 {
|
|
|
|
|
var exists bool
|
|
|
|
|
if err := pg.QueryRowContext(ctx, `
|
|
|
|
|
SELECT EXISTS (
|
|
|
|
|
SELECT 1
|
|
|
|
|
FROM mk_product_performance_grouped_snapshot_meta
|
|
|
|
|
WHERE report_key = $1
|
|
|
|
|
)
|
|
|
|
|
`, reportKey).Scan(&exists); err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
if !exists {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out = filterProductPerformancePreparedGroupedRows(out, effectiveFilters)
|
|
|
|
|
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
|
|
|
|
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
|
|
|
|
return out, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type productPerformanceGroupedSortNode struct {
|
|
|
|
|
Row map[string]any
|
|
|
|
|
Children []*productPerformanceGroupedSortNode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sortProductPerformancePreparedGroupedRows(rows []map[string]any, sortBy string, descending bool) []map[string]any {
|
|
|
|
|
sortBy = strings.TrimSpace(sortBy)
|
|
|
|
|
if len(rows) == 0 || sortBy == "" {
|
|
|
|
|
return rows
|
|
|
|
|
}
|
|
|
|
|
roots := make([]*productPerformanceGroupedSortNode, 0)
|
|
|
|
|
stack := make([]*productPerformanceGroupedSortNode, 0, 16)
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
node := &productPerformanceGroupedSortNode{Row: row}
|
|
|
|
|
level := intFromMap(row, "level")
|
|
|
|
|
if level < 0 {
|
|
|
|
|
level = 0
|
|
|
|
|
}
|
|
|
|
|
if level < len(stack) {
|
|
|
|
|
stack = stack[:level]
|
|
|
|
|
}
|
|
|
|
|
if level > 0 && level-1 < len(stack) {
|
|
|
|
|
stack[level-1].Children = append(stack[level-1].Children, node)
|
|
|
|
|
} else {
|
|
|
|
|
roots = append(roots, node)
|
|
|
|
|
}
|
|
|
|
|
if level >= len(stack) {
|
|
|
|
|
stack = append(stack, node)
|
|
|
|
|
} else {
|
|
|
|
|
stack[level] = node
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out := make([]map[string]any, 0, len(rows))
|
|
|
|
|
var appendNodes func(nodes []*productPerformanceGroupedSortNode)
|
|
|
|
|
appendNodes = func(nodes []*productPerformanceGroupedSortNode) {
|
|
|
|
|
sort.SliceStable(nodes, func(i, j int) bool {
|
|
|
|
|
cmp := compareProductPerformanceGroupedSortRows(nodes[i].Row, nodes[j].Row, sortBy)
|
|
|
|
|
if cmp == 0 {
|
|
|
|
|
cmp = strings.Compare(strings.ToLower(stringFromMap(nodes[i].Row, "label")), strings.ToLower(stringFromMap(nodes[j].Row, "label")))
|
|
|
|
|
}
|
|
|
|
|
if descending {
|
|
|
|
|
return cmp > 0
|
|
|
|
|
}
|
|
|
|
|
return cmp < 0
|
|
|
|
|
})
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
out = append(out, node.Row)
|
|
|
|
|
if len(node.Children) > 0 {
|
|
|
|
|
appendNodes(node.Children)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
appendNodes(roots)
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func compareProductPerformanceGroupedSortRows(left, right map[string]any, sortBy string) int {
|
|
|
|
|
leftValue := productPerformanceGroupedSortValue(left, sortBy)
|
|
|
|
|
rightValue := productPerformanceGroupedSortValue(right, sortBy)
|
|
|
|
|
leftNum, leftOK := numericProductPerformanceGroupedSortValue(leftValue)
|
|
|
|
|
rightNum, rightOK := numericProductPerformanceGroupedSortValue(rightValue)
|
|
|
|
|
if leftOK && rightOK {
|
|
|
|
|
switch {
|
|
|
|
|
case leftNum < rightNum:
|
|
|
|
|
return -1
|
|
|
|
|
case leftNum > rightNum:
|
|
|
|
|
return 1
|
|
|
|
|
default:
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
leftText := strings.ToLower(strings.TrimSpace(fmt.Sprint(leftValue)))
|
|
|
|
|
rightText := strings.ToLower(strings.TrimSpace(fmt.Sprint(rightValue)))
|
|
|
|
|
return strings.Compare(leftText, rightText)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSortValue(row map[string]any, sortBy string) any {
|
|
|
|
|
sortBy = strings.TrimSpace(sortBy)
|
|
|
|
|
if sortBy == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if value, ok := row[sortBy]; ok {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
switch sortBy {
|
|
|
|
|
case "performance_score_total", "performance_score_90d":
|
|
|
|
|
return row["performance_score"]
|
|
|
|
|
case "gross_margin_base_total":
|
|
|
|
|
return row["gross_margin_total"]
|
|
|
|
|
case "gross_margin_cost_total":
|
|
|
|
|
return row["gross_margin_total"]
|
|
|
|
|
case "color_yaka":
|
|
|
|
|
return mapGroupValue(row, "color_yaka")
|
|
|
|
|
case "market_key":
|
|
|
|
|
return mapGroupValue(row, "market_key")
|
|
|
|
|
default:
|
|
|
|
|
if sortBy == stringFromMap(row, "group_field") {
|
|
|
|
|
return stringFromMap(row, "group_value")
|
|
|
|
|
}
|
|
|
|
|
return stringFromMap(row, "label")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func numericProductPerformanceGroupedSortValue(value any) (float64, bool) {
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case nil:
|
|
|
|
|
return 0, false
|
|
|
|
|
case float64:
|
|
|
|
|
return v, true
|
|
|
|
|
case float32:
|
|
|
|
|
return float64(v), true
|
|
|
|
|
case int:
|
|
|
|
|
return float64(v), true
|
|
|
|
|
case int64:
|
|
|
|
|
return float64(v), true
|
|
|
|
|
case int32:
|
|
|
|
|
return float64(v), true
|
|
|
|
|
case json.Number:
|
|
|
|
|
f, err := v.Float64()
|
|
|
|
|
return f, err == nil
|
|
|
|
|
case string:
|
|
|
|
|
text := strings.TrimSpace(v)
|
|
|
|
|
if text == "" {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
parsed, err := strconv.ParseFloat(text, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return parsed, true
|
|
|
|
|
default:
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type productPerformanceSQLGroupFilter struct {
|
|
|
|
|
Field string
|
|
|
|
|
Value string
|
|
|
|
@@ -4018,6 +4481,139 @@ func filterProductPerformanceGroupedRows(rows []map[string]any, filters map[stri
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func filterProductPerformancePreparedGroupedRows(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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchedKeys := map[string]bool{}
|
|
|
|
|
allowedKeys := map[string]bool{}
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
matches := true
|
|
|
|
|
for field, allowed := range cleanFilters {
|
|
|
|
|
if !allowed[productPerformanceGroupedFilterValue(row, field)] {
|
|
|
|
|
matches = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !matches {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
key := stringFromMap(row, "key")
|
|
|
|
|
if key == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
matchedKeys[key] = true
|
|
|
|
|
for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
|
|
|
|
|
allowedKeys[ancestor] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(allowedKeys) == 0 {
|
|
|
|
|
return []map[string]any{}
|
|
|
|
|
}
|
|
|
|
|
out := make([]map[string]any, 0, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
key := stringFromMap(row, "key")
|
|
|
|
|
if allowedKeys[key] || productPerformanceGroupRowHasMatchedAncestor(key, matchedKeys) {
|
|
|
|
|
out = append(out, row)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupRowHasMatchedAncestor(key string, matchedKeys map[string]bool) bool {
|
|
|
|
|
for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
|
|
|
|
|
if matchedKeys[ancestor] {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func filterProductPerformanceGroupedRowsForExpansion(rows []map[string]any, expandedKeys map[string]bool, expandThroughLevel int, limit int) []map[string]any {
|
|
|
|
|
if len(rows) == 0 {
|
|
|
|
|
return rows
|
|
|
|
|
}
|
|
|
|
|
if limit <= 0 || limit > 50000 {
|
|
|
|
|
limit = 50000
|
|
|
|
|
}
|
|
|
|
|
out := make([]map[string]any, 0, minInt(limit, len(rows)))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
if !productPerformanceGroupedRowVisible(row, expandedKeys, expandThroughLevel) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
out = append(out, row)
|
|
|
|
|
if len(out) >= limit {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedRowVisible(row map[string]any, expandedKeys map[string]bool, expandThroughLevel int) bool {
|
|
|
|
|
level := intFromMap(row, "level")
|
|
|
|
|
if level <= 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
key := stringFromMap(row, "key")
|
|
|
|
|
if key == "" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ancestors := productPerformanceGroupKeyAncestors(key, false)
|
|
|
|
|
for ancestorLevel, ancestor := range ancestors {
|
|
|
|
|
if ancestorLevel <= expandThroughLevel {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if expandedKeys != nil && expandedKeys[ancestor] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupKeyAncestors(key string, includeSelf bool) []string {
|
|
|
|
|
parts := strings.Split(strings.TrimSpace(key), "|")
|
|
|
|
|
if len(parts) <= 1 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
last := len(parts) - 1
|
|
|
|
|
if !includeSelf {
|
|
|
|
|
last--
|
|
|
|
|
}
|
|
|
|
|
if last < 1 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := make([]string, 0, last)
|
|
|
|
|
for i := 1; i <= last; i++ {
|
|
|
|
|
out = append(out, strings.Join(parts[:i+1], "|"))
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceParentGroupKey(key string) string {
|
|
|
|
|
parts := strings.Split(strings.TrimSpace(key), "|")
|
|
|
|
|
if len(parts) <= 2 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts[:len(parts)-1], "|")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedFilterValue(row map[string]any, field string) string {
|
|
|
|
|
switch field {
|
|
|
|
|
case "urun_ana_grubu":
|
|
|
|
@@ -4037,6 +4633,12 @@ func productPerformanceGroupedFilterValue(row map[string]any, field string) stri
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
|
|
|
|
|
if rows, ok, err := productPerformanceGroupedSnapshotRows(ctx, pg, mode, limit); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
} else if ok {
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch mode {
|
|
|
|
|
case "products":
|
|
|
|
|
rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: limit, Page: 1, SortBy: "performance_score", Descending: true})
|
|
|
|
@@ -4062,6 +4664,74 @@ func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
|
|
|
|
|
reportKey, ok := productPerformanceGroupedSnapshotKey(mode)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
rows, exists, err := loadProductPerformanceSnapshotMapRows(ctx, pg, reportKey, limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if !exists {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(mode) == "idle" {
|
|
|
|
|
return productPerformanceIdleSourceRows(rows), nil
|
|
|
|
|
}
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, bool, error) {
|
|
|
|
|
reportKey, ok := productPerformanceGroupedSnapshotKey(mode)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
rows, exists, err := loadProductPerformanceSnapshotMapRows(ctx, pg, reportKey, limit)
|
|
|
|
|
if err != nil || !exists {
|
|
|
|
|
return rows, exists, err
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(mode) == "idle" {
|
|
|
|
|
return productPerformanceIdleSourceRows(rows), true, nil
|
|
|
|
|
}
|
|
|
|
|
return rows, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSnapshotReportKey(mode string, levels []string, mainGroup string) string {
|
|
|
|
|
levels = sanitizeProductPerformanceGroupLevels(levels)
|
|
|
|
|
if len(levels) == 0 {
|
|
|
|
|
levels = defaultProductPerformanceGroupLevels(mode)
|
|
|
|
|
}
|
|
|
|
|
parts := []string{"grouped", strings.TrimSpace(mode), productPerformanceGroupedLevelsKey(levels)}
|
|
|
|
|
if strings.TrimSpace(mainGroup) != "" {
|
|
|
|
|
parts = append(parts, strings.TrimSpace(mainGroup))
|
|
|
|
|
}
|
|
|
|
|
return productPerformanceSnapshotKey(parts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedLevelsKey(levels []string) string {
|
|
|
|
|
levels = sanitizeProductPerformanceGroupLevels(levels)
|
|
|
|
|
if len(levels) == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(levels, ">")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSnapshotKey(mode string) (string, bool) {
|
|
|
|
|
switch strings.TrimSpace(mode) {
|
|
|
|
|
case "products", "product_detail", "idle", "":
|
|
|
|
|
return productPerformanceSnapshotKey("products"), true
|
|
|
|
|
case "order_product_customers":
|
|
|
|
|
return productPerformanceSnapshotKey("order-product-customers"), true
|
|
|
|
|
case "order_market_details":
|
|
|
|
|
return productPerformanceSnapshotKey("order-market-details"), true
|
|
|
|
|
case "sales_color_yaka_market_customer", "sales_product_country_segment_market_customer", "sales_market_customer_product", "sales_country_segment_market_customer_product":
|
|
|
|
|
return productPerformanceSnapshotKey("sales-breakdown", productPerformanceSalesBreakdownMode(mode)), true
|
|
|
|
|
default:
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceSalesBreakdownMode(mode string) string {
|
|
|
|
|
switch strings.TrimSpace(mode) {
|
|
|
|
|
case "sales_color_yaka_market_customer":
|
|
|
|
@@ -4293,6 +4963,16 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|
|
|
|
if qty > 0 {
|
|
|
|
|
out["avg_price_usd_"+suffix] = sales / qty
|
|
|
|
|
}
|
|
|
|
|
basePrice, hasBasePrice := productPerformanceUnitCostForSuffix(out, "base_price_usd", suffix)
|
|
|
|
|
costPrice, hasCostPrice := productPerformanceUnitCostForSuffix(out, "cost_price_usd", suffix)
|
|
|
|
|
if qty > 0 && hasBasePrice {
|
|
|
|
|
out["unit_profit_base_"+suffix] = (sales / qty) - basePrice
|
|
|
|
|
out["gross_profit_base_usd_"+suffix] = sales - (qty * basePrice)
|
|
|
|
|
}
|
|
|
|
|
if qty > 0 && hasCostPrice {
|
|
|
|
|
out["unit_profit_cost_"+suffix] = (sales / qty) - costPrice
|
|
|
|
|
out["gross_profit_cost_usd_"+suffix] = sales - (qty * costPrice)
|
|
|
|
|
}
|
|
|
|
|
if sales > 0 {
|
|
|
|
|
if _, ok := out["gross_profit_base_usd_"+suffix]; ok {
|
|
|
|
|
out["gross_margin_base_"+suffix] = floatFromMap(out, "gross_profit_base_usd_"+suffix) / sales
|
|
|
|
@@ -4321,7 +5001,28 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|
|
|
|
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
|
|
|
|
|
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
|
|
|
|
|
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
|
|
|
|
|
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
|
|
|
|
|
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
|
|
|
|
|
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
|
|
|
|
|
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
|
|
|
|
|
out["performance_score"] = productPerformanceGroupScore(out, groupField)
|
|
|
|
|
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
|
|
|
|
score := productPerformanceOrderGroupScore(out)
|
|
|
|
|
out["performance_score_90d"] = score
|
|
|
|
|
out["performance_score_180d"] = score
|
|
|
|
|
out["performance_score_365d"] = score
|
|
|
|
|
out["performance_score_total"] = score
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceUnitCostForSuffix(row map[string]any, baseField, suffix string) (float64, bool) {
|
|
|
|
|
if value, ok := row[baseField+"_"+suffix]; ok {
|
|
|
|
|
return floatFromAny(value), true
|
|
|
|
|
}
|
|
|
|
|
if value, ok := row[baseField]; ok {
|
|
|
|
|
return floatFromAny(value), true
|
|
|
|
|
}
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupScore(row map[string]any, groupField string) float64 {
|
|
|
|
@@ -4344,11 +5045,18 @@ func isProductPerformanceCustomerGroup(groupField string) bool {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceSalesGroupScore(row map[string]any) float64 {
|
|
|
|
|
salesIndex := floatFromMap(row, "sales_index_90d")
|
|
|
|
|
margin := floatFromMap(row, "gross_margin_cost_90d")
|
|
|
|
|
invoiceCount := floatFromMap(row, "invoice_count_90d")
|
|
|
|
|
salesQty := floatFromMap(row, "sales_qty_90d")
|
|
|
|
|
stockTurnover := floatFromMap(row, "stock_turnover_90d")
|
|
|
|
|
return productPerformanceSalesPeriodScore(row, "90d")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float64 {
|
|
|
|
|
salesIndex := floatFromMap(row, "sales_index_"+suffix)
|
|
|
|
|
margin := floatFromMap(row, "gross_margin_cost_"+suffix)
|
|
|
|
|
if margin == 0 {
|
|
|
|
|
margin = floatFromMap(row, "gross_margin_"+suffix)
|
|
|
|
|
}
|
|
|
|
|
invoiceCount := floatFromMap(row, "invoice_count_"+suffix)
|
|
|
|
|
salesQty := floatFromMap(row, "sales_qty_"+suffix)
|
|
|
|
|
stockTurnover := floatFromMap(row, "stock_turnover_"+suffix)
|
|
|
|
|
if stockTurnover == 0 {
|
|
|
|
|
if stockQty := floatFromMap(row, "stock_qty"); stockQty > 0 {
|
|
|
|
|
stockTurnover = salesQty / stockQty
|
|
|
|
@@ -4462,23 +5170,23 @@ func sanitizeProductPerformanceGroupLevels(levels []string) []string {
|
|
|
|
|
func defaultProductPerformanceGroupLevels(mode string) []string {
|
|
|
|
|
switch mode {
|
|
|
|
|
case "sales_color_yaka_market_customer":
|
|
|
|
|
return []string{"color_yaka", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
|
|
|
|
return []string{"color_yaka", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
|
|
|
|
case "product_detail":
|
|
|
|
|
return []string{"urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka", "market_key"}
|
|
|
|
|
return []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"}
|
|
|
|
|
case "idle":
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka"}
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
|
|
|
|
|
case "sales_product_country_segment_market_customer":
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka"}
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}
|
|
|
|
|
case "sales_market_customer_product":
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka"}
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
|
|
|
|
|
case "sales_country_segment_market_customer_product":
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka"}
|
|
|
|
|
return []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
|
|
|
|
|
case "order_product_customers":
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka"}
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}
|
|
|
|
|
case "order_market_details":
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka"}
|
|
|
|
|
return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
|
|
|
|
|
default:
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_yaka", "market_key"}
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4671,6 +5379,10 @@ func floatFromMap(row map[string]any, field string) float64 {
|
|
|
|
|
return floatFromAny(row[field])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func intFromMap(row map[string]any, field string) int {
|
|
|
|
|
return int(floatFromAny(row[field]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func floatFromAny(value any) float64 {
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case float64:
|
|
|
|
@@ -4689,6 +5401,13 @@ func floatFromAny(value any) float64 {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func minInt(a, b int) int {
|
|
|
|
|
if a < b {
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ListProductPerformanceSalesDetails(ctx context.Context, pg *sql.DB, productCode, colorCode, yakaKodu string, limit int) ([]models.ProductPerformanceSalesDetailRow, error) {
|
|
|
|
|
if limit <= 0 || limit > 500 {
|
|
|
|
|
limit = 100
|
|
|
|
|