Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 02:58:06 +03:00
parent 480cad0d6f
commit 48bde09c2c
4 changed files with 562 additions and 70 deletions
+2 -1
View File
@@ -66,10 +66,11 @@ func main() {
log.Fatalf("refresh failed: %v", err) log.Fatalf("refresh failed: %v", err)
} }
fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d\n", fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d snapshots=%d\n",
result.SalesRows, result.SalesRows,
result.StockRows, result.StockRows,
result.KpiRows, result.KpiRows,
result.SnapshotRows,
) )
} }
+94 -2
View File
@@ -26,6 +26,12 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00") deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00")
deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24) deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24)
runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false) runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false)
fullEnabled := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_ENABLED", false)
fullWeekday := productPerformanceEnvWeekday("PRODUCT_PERFORMANCE_FULL_WEEKDAY", time.Saturday)
fullHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_FULL_HHMM", "16:00")
fullStartDate := productPerformanceEnvDate("PRODUCT_PERFORMANCE_FULL_START_DATE", time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local))
fullTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_FULL_TIMEOUT_HOURS", 18, 1, 72)
fullRunOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_RUN_ON_STARTUP", false)
var running int32 var running int32
runDelta := func(reason string) { runDelta := func(reason string) {
@@ -49,12 +55,40 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err) log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return return
} }
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d duration_ms=%d", log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.DurationMS) reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
}
runFull := func(reason string) {
if !atomic.CompareAndSwapInt32(&running, 0, 1) {
log.Printf("[ProductPerformanceJob] skip (%s): already running", reason)
return
}
defer atomic.StoreInt32(&running, 0)
endDate := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(fullTimeoutHours)*time.Hour)
defer cancel()
result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{
Mode: "full",
Stage: "all",
StartDate: fullStartDate,
EndDate: endDate,
})
if err != nil {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return
}
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
} }
log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t", log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t",
deltaHHMM, deltaDays, runOnStartup) deltaHHMM, deltaDays, runOnStartup)
if fullEnabled {
log.Printf("[ProductPerformanceJob] scheduled weekly_full=%s %s start_date=%s timeout_hours=%d run_full_on_startup=%t",
fullWeekday.String(), fullHHMM, fullStartDate.Format("2006-01-02"), fullTimeoutHours, fullRunOnStartup)
}
go func() { go func() {
if runOnStartup { if runOnStartup {
@@ -69,6 +103,21 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
runDelta("daily-delta") runDelta("daily-delta")
} }
}() }()
if fullEnabled {
go func() {
if fullRunOnStartup {
time.Sleep(30 * time.Second)
runFull("startup-full")
}
for {
next := productPerformanceNextWeekly(time.Now(), fullWeekday, fullHHMM)
log.Printf("[ProductPerformanceJob] weekly full next_at=%s in=%s", next.Format(time.RFC3339), time.Until(next).Round(time.Second))
time.Sleep(time.Until(next))
runFull("weekly-full")
}
}()
}
} }
func productPerformanceNextDaily(now time.Time, hhmm string) time.Time { func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
@@ -80,6 +129,16 @@ func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
return next return next
} }
func productPerformanceNextWeekly(now time.Time, weekday time.Weekday, hhmm string) time.Time {
hour, minute := productPerformanceParseHHMM(hhmm, 16, 0)
daysUntil := (int(weekday) - int(now.Weekday()) + 7) % 7
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()).AddDate(0, 0, daysUntil)
if !next.After(now) {
next = next.AddDate(0, 0, 7)
}
return next
}
func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) { func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) {
parts := strings.Split(strings.TrimSpace(raw), ":") parts := strings.Split(strings.TrimSpace(raw), ":")
if len(parts) != 2 { if len(parts) != 2 {
@@ -120,3 +179,36 @@ func productPerformanceEnvBool(name string, fallback bool) bool {
} }
return raw == "1" || raw == "true" || raw == "on" || raw == "yes" return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
} }
func productPerformanceEnvDate(name string, fallback time.Time) time.Time {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := time.ParseInLocation("2006-01-02", raw, time.Local)
if err != nil {
return fallback
}
return parsed
}
func productPerformanceEnvWeekday(name string, fallback time.Weekday) time.Weekday {
switch strings.ToLower(strings.TrimSpace(os.Getenv(name))) {
case "sunday", "sun", "pazar", "0":
return time.Sunday
case "monday", "mon", "pazartesi", "1":
return time.Monday
case "tuesday", "tue", "sali", "salı", "2":
return time.Tuesday
case "wednesday", "wed", "carsamba", "çarşamba", "3":
return time.Wednesday
case "thursday", "thu", "persembe", "perşembe", "4":
return time.Thursday
case "friday", "fri", "cuma", "5":
return time.Friday
case "saturday", "sat", "cumartesi", "6":
return time.Saturday
default:
return fallback
}
}
+372 -16
View File
@@ -8,6 +8,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"os"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -39,16 +40,19 @@ type ProductPerformanceRefreshRequest struct {
} }
type ProductPerformanceRefreshResult struct { type ProductPerformanceRefreshResult struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Stage string `json:"stage"` Stage string `json:"stage"`
StartDate string `json:"start_date"` StartDate string `json:"start_date"`
EndDate string `json:"end_date"` EndDate string `json:"end_date"`
SalesRows int `json:"sales_rows"` SalesRows int `json:"sales_rows"`
StockRows int `json:"stock_rows"` StockRows int `json:"stock_rows"`
KpiRows int `json:"kpi_rows"` KpiRows int `json:"kpi_rows"`
DurationMS int64 `json:"duration_ms"` SnapshotRows int `json:"snapshot_rows"`
DurationMS int64 `json:"duration_ms"`
} }
type productPerformanceSnapshotBypassKey struct{}
func productPerformanceStockQuery() string { func productPerformanceStockQuery() string {
return ` return `
;WITH ActiveWarehouses AS ( ;WITH ActiveWarehouses AS (
@@ -561,6 +565,23 @@ WHERE btrim(askili_yan) = '-'`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot (
report_key TEXT NOT NULL,
row_order INTEGER NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_mk_product_performance_report_snapshot PRIMARY KEY (report_key, row_order)
)`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_report_snapshot_key ON mk_product_performance_report_snapshot (report_key, row_order)`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot_meta (
report_key TEXT PRIMARY KEY,
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 { for _, stmt := range stmts {
if _, err := pg.Exec(stmt); err != nil { if _, err := pg.Exec(stmt); err != nil {
@@ -570,6 +591,264 @@ WHERE btrim(askili_yan) = '-'`,
return nil return nil
} }
func productPerformanceSnapshotBypass(ctx context.Context) context.Context {
return context.WithValue(ctx, productPerformanceSnapshotBypassKey{}, true)
}
func productPerformanceUseSnapshot(ctx context.Context) bool {
v, _ := ctx.Value(productPerformanceSnapshotBypassKey{}).(bool)
return !v
}
func productPerformanceLiveFallbackEnabled() bool {
raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_PERFORMANCE_LIVE_FALLBACK")))
return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}
func productPerformanceSnapshotKey(parts ...string) string {
cleaned := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.ToLower(strings.TrimSpace(part))
if part != "" {
cleaned = append(cleaned, part)
}
}
return strings.Join(cleaned, ":")
}
func loadProductPerformanceSnapshotRows[T any](ctx context.Context, pg *sql.DB, reportKey string, limit int) ([]T, 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([]T, 0, limit)
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, false, err
}
var item T
if err := json.Unmarshal(raw, &item); err != nil {
return nil, false, err
}
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)
if err != nil || !ok || len(rows) == 0 {
return zero, ok, err
}
return rows[0], true, nil
}
func saveProductPerformanceSnapshotRows[T any](ctx context.Context, pg *sql.DB, reportKey string, rows []T, 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_report_snapshot WHERE report_key = $1`, reportKey); err != nil {
return err
}
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO mk_product_performance_report_snapshot (report_key, row_order, payload, updated_at)
VALUES ($1, $2, $3, now())
`)
if err != nil {
return err
}
defer stmt.Close()
for i, row := range rows {
raw, err := json.Marshal(row)
if err != nil {
return err
}
if _, err := stmt.ExecContext(ctx, reportKey, i, raw); err != nil {
return err
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_report_snapshot_meta (report_key, row_count, refreshed_at, duration_ms)
VALUES ($1, $2, now(), $3)
ON CONFLICT (report_key) DO UPDATE SET
row_count = EXCLUDED.row_count,
refreshed_at = EXCLUDED.refreshed_at,
duration_ms = EXCLUDED.duration_ms
`, reportKey, 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
}
ctx = productPerformanceSnapshotBypass(ctx)
total := 0
save := func(reportKey string, rows any, started time.Time) error {
switch v := rows.(type) {
case []models.ProductPerformanceSummary:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceGeneralRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderAnalysisRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderGroupRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderProductCustomerRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderMarketDetailRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceMarketRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceCountryRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceCustomerRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceSalesBreakdownRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
default:
return fmt.Errorf("unsupported product performance snapshot payload %s", reportKey)
}
}
started := time.Now()
if summary, err := GetProductPerformanceSummary(ctx, pg); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("summary"), []models.ProductPerformanceSummary{summary}, started); err != nil {
return total, err
}
started = time.Now()
if rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: 50000, Page: 1, SortBy: "performance_score", Descending: true}); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("products"), rows, started); err != nil {
return total, err
}
started = time.Now()
if rows, err := ListProductPerformanceGeneral(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("general"), rows, started); err != nil {
return total, err
}
started = time.Now()
if rows, err := ListProductPerformanceOrderAnalysis(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("orders"), rows, started); err != nil {
return total, err
}
for _, mode := range []string{"market", "customer"} {
started = time.Now()
rows, err := ListProductPerformanceOrderGroups(ctx, pg, mode, 50000)
if err != nil {
return total, err
}
if err := save(productPerformanceSnapshotKey("order-groups", mode), rows, started); err != nil {
return total, err
}
}
started = time.Now()
if rows, err := ListProductPerformanceOrderProductCustomers(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("order-product-customers"), rows, started); err != nil {
return total, err
}
started = time.Now()
if rows, err := ListProductPerformanceOrderMarketDetails(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("order-market-details"), rows, started); err != nil {
return total, err
}
started = time.Now()
if rows, err := ListProductPerformanceMarkets(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("markets"), rows, started); err != nil {
return total, err
}
started = time.Now()
if rows, err := ListProductPerformanceCountries(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("countries"), rows, started); err != nil {
return total, err
}
for _, mode := range []string{"market_customer", "country_customer", "market_country_customer"} {
started = time.Now()
rows, err := ListProductPerformanceCustomers(ctx, pg, mode, 50000)
if err != nil {
return total, err
}
if err := save(productPerformanceSnapshotKey("customers", mode), rows, started); err != nil {
return total, err
}
}
for _, mode := range []string{"color_yaka_market_customer", "product_country_segment_market_customer", "market_customer_product", "country_segment_market_customer_product"} {
started = time.Now()
rows, err := ListProductPerformanceSalesBreakdown(ctx, pg, mode, 50000)
if err != nil {
return total, err
}
if err := save(productPerformanceSnapshotKey("sales-breakdown", mode), rows, started); err != nil {
return total, err
}
}
return total, nil
}
func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) { func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
started := time.Now() started := time.Now()
stage := productPerformanceRefreshStage(req.Stage) stage := productPerformanceRefreshStage(req.Stage)
@@ -689,6 +968,7 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
} }
var kpiRows int var kpiRows int
var snapshotRows int
if shouldRun("kpi") { if shouldRun("kpi") {
if err := runStage("kpi", func(tx *sql.Tx) error { if err := runStage("kpi", func(tx *sql.Tx) error {
log.Printf("[ProductPerformanceRefresh] kpi rebuild start") log.Printf("[ProductPerformanceRefresh] kpi rebuild start")
@@ -702,20 +982,28 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
}); err != nil { }); err != nil {
return ProductPerformanceRefreshResult{}, err return ProductPerformanceRefreshResult{}, err
} }
log.Printf("[ProductPerformanceRefresh] report snapshots rebuild start")
rows, err := RebuildProductPerformanceReportSnapshots(ctx, pg)
if err != nil {
return ProductPerformanceRefreshResult{}, err
}
snapshotRows = rows
log.Printf("[ProductPerformanceRefresh] report snapshots rebuild done rows=%d elapsed=%s", snapshotRows, time.Since(started).Round(time.Second))
} else { } else {
log.Printf("[ProductPerformanceRefresh] kpi skipped stage=%s", stage) log.Printf("[ProductPerformanceRefresh] kpi skipped stage=%s", stage)
} }
log.Printf("[ProductPerformanceRefresh] refresh done total_elapsed=%s", time.Since(started).Round(time.Second)) log.Printf("[ProductPerformanceRefresh] refresh done total_elapsed=%s", time.Since(started).Round(time.Second))
return ProductPerformanceRefreshResult{ return ProductPerformanceRefreshResult{
Mode: mode, Mode: mode,
Stage: stage, Stage: stage,
StartDate: req.StartDate.Format("2006-01-02"), StartDate: req.StartDate.Format("2006-01-02"),
EndDate: req.EndDate.Format("2006-01-02"), EndDate: req.EndDate.Format("2006-01-02"),
SalesRows: salesRows, SalesRows: salesRows,
StockRows: stockRows, StockRows: stockRows,
KpiRows: kpiRows, KpiRows: kpiRows,
DurationMS: time.Since(started).Milliseconds(), SnapshotRows: snapshotRows,
DurationMS: time.Since(started).Milliseconds(),
}, nil }, nil
} }
@@ -1168,6 +1456,13 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
if page <= 0 { if page <= 0 {
page = 1 page = 1
} }
if !productPerformanceHasServerFilters(f) {
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceRow](ctx, pg, productPerformanceSnapshotKey("products"), limit); err != nil {
return nil, 0, err
} else if ok {
return rows, len(rows), nil
}
}
where, args := productPerformanceWhere(f) where, args := productPerformanceWhere(f)
countQuery := `SELECT COUNT(*) FROM mk_product_performance_kpi_daily WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + where countQuery := `SELECT COUNT(*) FROM mk_product_performance_kpi_daily WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + where
var total int var total int
@@ -1237,6 +1532,11 @@ func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.Produ
if err := EnsureProductPerformanceTables(pg); err != nil { if err := EnsureProductPerformanceTables(pg); err != nil {
return models.ProductPerformanceSummary{}, err return models.ProductPerformanceSummary{}, err
} }
if summary, ok, err := loadProductPerformanceSnapshotItem[models.ProductPerformanceSummary](ctx, pg, productPerformanceSnapshotKey("summary")); err != nil {
return models.ProductPerformanceSummary{}, err
} else if ok {
return summary, nil
}
var s models.ProductPerformanceSummary var s models.ProductPerformanceSummary
err := pg.QueryRowContext(ctx, ` err := pg.QueryRowContext(ctx, `
WITH Latest AS ( WITH Latest AS (
@@ -1307,6 +1607,11 @@ func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) (
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceGeneralRow](ctx, pg, productPerformanceSnapshotKey("general"), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := pg.QueryContext(ctx, ` rows, err := pg.QueryContext(ctx, `
WITH Bounds AS ( WITH Bounds AS (
SELECT SELECT
@@ -1615,6 +1920,11 @@ func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderAnalysisRow](ctx, pg, productPerformanceSnapshotKey("orders"), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := db.MssqlDB.QueryContext(ctx, ` rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS ( WITH OpenOrderLines AS (
@@ -1917,6 +2227,11 @@ func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdow
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderGroupRow](ctx, pg, productPerformanceSnapshotKey("order-groups", mode), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := db.MssqlDB.QueryContext(ctx, ` rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS ( WITH OpenOrderLines AS (
@@ -2153,6 +2468,11 @@ func ListProductPerformanceOrderProductCustomers(ctx context.Context, pg *sql.DB
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderProductCustomerRow](ctx, pg, productPerformanceSnapshotKey("order-product-customers"), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := db.MssqlDB.QueryContext(ctx, ` rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS ( WITH OpenOrderLines AS (
SELECT SELECT
@@ -2291,6 +2611,11 @@ func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, l
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderMarketDetailRow](ctx, pg, productPerformanceSnapshotKey("order-market-details"), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := db.MssqlDB.QueryContext(ctx, ` rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS ( WITH OpenOrderLines AS (
SELECT SELECT
@@ -2414,6 +2739,11 @@ func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) (
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceMarketRow](ctx, pg, productPerformanceSnapshotKey("markets"), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := pg.QueryContext(ctx, ` rows, err := pg.QueryContext(ctx, `
SELECT SELECT
market_key, market_key,
@@ -2472,6 +2802,11 @@ func ListProductPerformanceCountries(ctx context.Context, pg *sql.DB, limit int)
} else if limit > 50000 { } else if limit > 50000 {
limit = 50000 limit = 50000
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceCountryRow](ctx, pg, productPerformanceSnapshotKey("countries"), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
rows, err := pg.QueryContext(ctx, ` rows, err := pg.QueryContext(ctx, `
SELECT SELECT
customer_country, customer_country,
@@ -2549,6 +2884,11 @@ func ListProductPerformanceCustomers(ctx context.Context, pg *sql.DB, breakdown
selectMarket = "market_key" selectMarket = "market_key"
groupCols = append(groupCols, "market_key") groupCols = append(groupCols, "market_key")
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceCustomerRow](ctx, pg, productPerformanceSnapshotKey("customers", mode), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT SELECT
@@ -2709,6 +3049,12 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
groupCols = []string{"CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END", "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"} groupCols = []string{"CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END", "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
} }
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceSalesBreakdownRow](ctx, pg, productPerformanceSnapshotKey("sales-breakdown", mode), limit); err != nil {
return nil, err
} else if ok {
return rows, nil
}
selectExpr := func(name string) string { selectExpr := func(name string) string {
if selected[name] { if selected[name] {
return fields[name] + " AS " + name return fields[name] + " AS " + name
@@ -4404,6 +4750,16 @@ func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
return strings.Join(parts, ""), args return strings.Join(parts, ""), args
} }
func productPerformanceHasServerFilters(f ProductPerformanceFilters) bool {
return strings.TrimSpace(f.Search) != "" ||
strings.TrimSpace(f.ProductCode) != "" ||
strings.TrimSpace(f.MarketKey) != "" ||
strings.TrimSpace(f.Kategori) != "" ||
strings.TrimSpace(f.Seri) != "" ||
strings.TrimSpace(f.Bucket) != "" ||
f.Page > 1
}
func productPerformanceOrderBy(sortBy string, desc bool) string { func productPerformanceOrderBy(sortBy string, desc bool) string {
allowed := map[string]string{ allowed := map[string]string{
"product_code": "product_code", "product_code": "product_code",
@@ -1611,10 +1611,10 @@ const selectedExpandLevelKeysByTab = reactive({
product_detail: ['urun_alt_grubu'], product_detail: ['urun_alt_grubu'],
sales_color_yaka_market_customer: ['color_yaka'], sales_color_yaka_market_customer: ['color_yaka'],
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori'], idle: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'], sales_product_country_segment_market_customer: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
sales_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'], sales_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
sales_country_segment_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'], sales_country_segment_market_customer_product: ['country', 'customer_segment', 'market_key'],
order_product_customers: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'], order_product_customers: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
order_market_details: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'] order_market_details: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan']
}) })
const productKpiFetchLimit = 50000 const productKpiFetchLimit = 50000
@@ -1623,6 +1623,7 @@ const maxBulkExpandKeys = 1000
const maxBackendExpandedKeys = 1000 const maxBackendExpandedKeys = 1000
const tableVirtualSliceSize = 160 const tableVirtualSliceSize = 160
const productThumbVirtualItemSize = 236 const productThumbVirtualItemSize = 236
const maxRenderedGroupedRows = 2500
const maxAutoExpandLevelByTab = { const maxAutoExpandLevelByTab = {
products: 8, products: 8,
product_detail: 8, product_detail: 8,
@@ -2554,16 +2555,18 @@ const tabGroupLevels = {
{ key: 'color_yaka', label: 'Renk/Yaka' } { key: 'color_yaka', label: 'Renk/Yaka' }
], ],
sales_product_country_segment_market_customer: [ sales_product_country_segment_market_customer: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' }, { key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
{ key: 'askili_yan', label: 'Askılı/Yan' }, { key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'kategori', label: 'Kategori' }, { key: 'kategori', label: 'Kategori' },
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' }, { key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' },
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' }, { key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
{ key: 'product_code', label: 'Ürün' }, { key: 'product_code', label: 'Ürün' },
{ key: 'color_yaka', label: 'Renk/Yaka' } { key: 'color_yaka', label: 'Renk/Yaka' },
{ key: 'country', label: 'Ülke' },
{ key: 'customer_segment', label: 'Segment' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' }
], ],
sales_market_customer_product: [ sales_market_customer_product: [
{ key: 'market_key', label: 'Piyasa' }, { key: 'market_key', label: 'Piyasa' },
@@ -2578,6 +2581,8 @@ const tabGroupLevels = {
{ key: 'color_yaka', label: 'Renk/Yaka' } { key: 'color_yaka', label: 'Renk/Yaka' }
], ],
sales_country_segment_market_customer_product: [ sales_country_segment_market_customer_product: [
{ key: 'country', label: 'Ülke' },
{ key: 'customer_segment', label: 'Segment' },
{ key: 'market_key', label: 'Piyasa' }, { key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri Kodu' }, { key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' }, { key: 'customer_name', label: 'Müşteri' },
@@ -2590,16 +2595,16 @@ const tabGroupLevels = {
{ key: 'color_yaka', label: 'Renk/Yaka' } { key: 'color_yaka', label: 'Renk/Yaka' }
], ],
order_product_customers: [ order_product_customers: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' }, { key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
{ key: 'askili_yan', label: 'Askılı/Yan' }, { key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'kategori', label: 'Kategori' }, { key: 'kategori', label: 'Kategori' },
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' }, { key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' },
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' }, { key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
{ key: 'product_code', label: 'Ürün' }, { key: 'product_code', label: 'Ürün' },
{ key: 'color_yaka', label: 'Renk/Yaka' } { key: 'color_yaka', label: 'Renk/Yaka' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' }
], ],
order_market_details: [ order_market_details: [
{ key: 'market_key', label: 'Piyasa' }, { key: 'market_key', label: 'Piyasa' },
@@ -2660,22 +2665,6 @@ const fullExpandThroughLevel = computed(() => {
return Math.min(lastGroupLevel, maxAutoExpandThroughLevelForTab()) return Math.min(lastGroupLevel, maxAutoExpandThroughLevelForTab())
}) })
const tableFilterSourceMap = computed(() => ({
products: { rows: filterSourceRowsForTab('products', rows.value), columns: productColumns.value },
product_detail: { rows: filterSourceRowsForTab('product_detail', detailProductRows.value), columns: productDetailColumns.value },
general: { rows: generalRows.value, columns: generalColumns },
order_product_customers: { rows: filterSourceRowsForTab('order_product_customers', orderProductCustomerRows.value), columns: orderProductCustomerColumns },
order_market_details: { rows: filterSourceRowsForTab('order_market_details', orderMarketDetailRows.value), columns: orderMarketDetailColumns },
idle: { rows: filterSourceRowsForTab('idle', idleRows.value), columns: idleColumns },
markets: { rows: marketRows.value, columns: marketColumns },
countries: { rows: countryRows.value, columns: countryColumns },
customers: { rows: customerRows.value, columns: customerColumns },
sales_color_yaka_market_customer: { rows: filterSourceRowsForTab('sales_color_yaka_market_customer', salesBreakdownRows.sales_color_yaka_market_customer), columns: visibleSalesBreakdownColumns.value },
sales_product_country_segment_market_customer: { rows: filterSourceRowsForTab('sales_product_country_segment_market_customer', salesBreakdownRows.sales_product_country_segment_market_customer), columns: visibleSalesBreakdownColumns.value },
sales_market_customer_product: { rows: filterSourceRowsForTab('sales_market_customer_product', salesBreakdownRows.sales_market_customer_product), columns: visibleSalesBreakdownColumns.value },
sales_country_segment_market_customer_product: { rows: filterSourceRowsForTab('sales_country_segment_market_customer_product', salesBreakdownRows.sales_country_segment_market_customer_product), columns: visibleSalesBreakdownColumns.value }
}))
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value)) const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value))
const isProductKpiTab = computed(() => activeTab.value === 'products' || activeTab.value === 'product_detail') const isProductKpiTab = computed(() => activeTab.value === 'products' || activeTab.value === 'product_detail')
const activeProductTableKey = computed(() => activeTab.value === 'product_detail' ? 'product_detail' : 'products') const activeProductTableKey = computed(() => activeTab.value === 'product_detail' ? 'product_detail' : 'products')
@@ -2706,7 +2695,7 @@ const activeGroupSourceRows = computed(() => {
const columnFilterOptionMap = computed(() => { const columnFilterOptionMap = computed(() => {
const out = {} const out = {}
const tableKey = isProductKpiTab.value ? activeProductTableKey.value : activeTab.value const tableKey = isProductKpiTab.value ? activeProductTableKey.value : activeTab.value
const source = tableFilterSourceMap.value[tableKey] const source = tableFilterSource(tableKey)
if (!source) return out if (!source) return out
out[tableKey] = {} out[tableKey] = {}
for (const col of source.columns) { for (const col of source.columns) {
@@ -2730,7 +2719,7 @@ const columnFilterOptionMap = computed(() => {
const productTableRows = computed(() => { const productTableRows = computed(() => {
const out = [] const out = []
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels) appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels, 'products')
return out return out
}) })
const detailProductTableRows = computed(() => buildGroupedTableRows('product_detail', filteredDetailProductRows.value)) const detailProductTableRows = computed(() => buildGroupedTableRows('product_detail', filteredDetailProductRows.value))
@@ -2813,13 +2802,14 @@ const allProductGroupsExpanded = computed(() => {
return selectedExpandThroughLevel.value >= fullExpandThroughLevel.value return selectedExpandThroughLevel.value >= fullExpandThroughLevel.value
}) })
function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) { function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels, tableKey = activeTab.value) {
appendGroupRowsAt(out, sourceRows, level, 0, parentKeys, levels) appendGroupRowsAt(out, sourceRows, level, 0, parentKeys, levels, tableKey)
} }
function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, levels = groupLevels) { function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, levels = groupLevels, tableKey = activeTab.value) {
if (out.length >= maxRenderedGroupedRows) return
if (level >= levels.length) { if (level >= levels.length) {
out.push(...sortProductLeafRows(sourceRows)) out.push(...sortLeafRowsForTable(tableKey, sourceRows).slice(0, Math.max(0, maxRenderedGroupedRows - out.length)))
return return
} }
@@ -2832,16 +2822,17 @@ function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, lev
} }
Array.from(grouped.entries()) Array.from(grouped.entries())
.sort((a, b) => a[0].localeCompare(b[0], 'tr')) .sort((a, b) => compareGroupedBucketsForTable(tableKey, a, b, groupDef))
.forEach(([value, groupRows]) => { .forEach(([value, groupRows]) => {
if (out.length >= maxRenderedGroupedRows) return
if (shouldSkipGroupValue(groupDef.key, value)) { if (shouldSkipGroupValue(groupDef.key, value)) {
appendGroupRowsAt(out, groupRows, level + 1, visualLevel, parentKeys, levels) appendGroupRowsAt(out, groupRows, level + 1, visualLevel, parentKeys, levels, tableKey)
return return
} }
const key = [...parentKeys, `${groupDef.key}:${value}`].join('|') const key = [...parentKeys, `${groupDef.key}:${value}`].join('|')
out.push(makeGroupRow(key, visualLevel, groupDef, value, groupRows)) out.push(makeGroupRow(key, visualLevel, groupDef, value, groupRows))
if (isGroupExpanded(key)) { if (isGroupExpanded(key)) {
appendGroupRowsAt(out, groupRows, level + 1, visualLevel + 1, [...parentKeys, `${groupDef.key}:${value}`], levels) appendGroupRowsAt(out, groupRows, level + 1, visualLevel + 1, [...parentKeys, `${groupDef.key}:${value}`], levels, tableKey)
} }
}) })
} }
@@ -2849,7 +2840,7 @@ function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, lev
function buildGroupedTableRows (tableKey, sourceRows) { function buildGroupedTableRows (tableKey, sourceRows) {
const levels = tabGroupLevels[tableKey] || groupLevels const levels = tabGroupLevels[tableKey] || groupLevels
const out = [] const out = []
appendGroupRows(out, sourceRows, 0, [`tab:${tableKey}`], levels) appendGroupRows(out, sourceRows, 0, [`tab:${tableKey}`], levels, tableKey)
return out return out
} }
@@ -2863,6 +2854,22 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
return fallbackRows return fallbackRows
} }
function tableFilterSource (tableKey) {
if (tableKey === 'products') return { rows: filterSourceRowsForTab('products', rows.value), columns: productColumns.value }
if (tableKey === 'product_detail') return { rows: filterSourceRowsForTab('product_detail', detailProductRows.value), columns: productDetailColumns.value }
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 === 'countries') return { rows: countryRows.value, columns: countryColumns }
if (tableKey === 'customers') return { rows: customerRows.value, columns: customerColumns }
if (salesBreakdownTabKeys.includes(tableKey)) {
return { rows: filterSourceRowsForTab(tableKey, salesBreakdownRows[tableKey] || []), columns: visibleSalesBreakdownColumns.value }
}
return null
}
function columnsForTableKey (tabKey) { function columnsForTableKey (tabKey) {
if (tabKey === 'products') return productColumns.value if (tabKey === 'products') return productColumns.value
if (tabKey === 'product_detail') return productDetailColumns.value if (tabKey === 'product_detail') return productDetailColumns.value
@@ -2915,6 +2922,45 @@ function sortProductLeafRows (sourceRows) {
}) })
} }
function sortLeafRowsForTable (tableKey, sourceRows) {
const { sortBy, descending } = tableSortState(tableKey)
if (!sortBy) return sortProductLeafRows(sourceRows)
return sortFlatTableRows(sourceRows, sortBy, descending)
}
function tableSortState (tableKey) {
const pagination = tablePagination[tableKey] || {}
return {
sortBy: pagination.sortBy || '',
descending: pagination.descending !== false
}
}
function compareGroupedBucketsForTable (tableKey, left, right, groupDef) {
const [leftLabel, leftRows] = left
const [rightLabel, rightRows] = right
const { sortBy, descending } = tableSortState(tableKey)
if (!sortBy || sortBy === groupDef.key) {
return String(leftLabel).localeCompare(String(rightLabel), 'tr', { numeric: true, sensitivity: 'base' })
}
const leftValue = groupedBucketSortValue(leftRows, sortBy)
const rightValue = groupedBucketSortValue(rightRows, sortBy)
const cmp = compareColumnSortValues(leftValue, rightValue)
if (cmp !== 0) return descending ? -cmp : cmp
return String(leftLabel).localeCompare(String(rightLabel), 'tr', { numeric: true, sensitivity: 'base' })
}
function groupedBucketSortValue (sourceRows, sortBy) {
if (!sourceRows?.length) return 0
if (sortBy === 'stock_qty') return distinctVariantStockQty(sourceRows)
if (sortBy === 'idle_cost_usd') return distinctVariantStockCost(sourceRows)
if (sortBy === 'base_price_usd' || sortBy === 'cost_price_usd') return weightedAverageProductCost(sourceRows, sortBy)
if (shouldSumField(sortBy)) return sumRows(sourceRows, sortBy)
if (shouldAverageField(sortBy)) return weightedAverageOrAverage(sourceRows, sortBy, weightFieldForMetric(sortBy))
return sortValueForColumnName(sourceRows[0], sortBy)
}
function sortProductGroupedTableRows (tableRows, sortBy, descending) { function sortProductGroupedTableRows (tableRows, sortBy, descending) {
const source = Array.isArray(tableRows) ? tableRows : [] const source = Array.isArray(tableRows) ? tableRows : []
if (!sortBy || !source.some(row => row?.__group)) { if (!sortBy || !source.some(row => row?.__group)) {
@@ -3437,13 +3483,10 @@ function marginFromRows (sourceRows, salesField, qtyField, unitCostField) {
} }
function normalizeGroupValue (value) { function normalizeGroupValue (value) {
return String(value || '').trim() return String(value || '').trim() || '(Boş)'
} }
function shouldSkipGroupValue (field, value) { function shouldSkipGroupValue (field, value) {
const text = String(value || '').trim()
if (field === 'urun_ilk_grubu') return !text || text === '-'
if (field === 'askili_yan') return !text || text === '-'
return false return false
} }
@@ -3709,14 +3752,15 @@ function syncExpansionAfterFilterChange () {
} }
function filterRowsForTable (tableKey, sourceRows, sourceColumns) { function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
return sourceRows.filter(row => { const activeFilters = sourceColumns
return sourceColumns.every(col => { .filter(col => isColumnFilterable(col.name))
if (!isColumnFilterable(col.name)) return true .map(col => ({
const selected = selectedColumnFilters(tableKey, col.name) col,
if (!selected.length) return true selected: new Set(selectedColumnFilters(tableKey, col.name))
return selected.includes(columnFilterValue(row, col)) }))
}) .filter(item => item.selected.size > 0)
}) if (!activeFilters.length) return sourceRows
return sourceRows.filter(row => activeFilters.every(item => item.selected.has(columnFilterValue(row, item.col))))
} }
function columnFilterValue (row, col) { function columnFilterValue (row, col) {
@@ -4465,7 +4509,6 @@ function setupTopScrollbarSync () {
function switchPerformanceTab (tabName) { function switchPerformanceTab (tabName) {
if (!tabName || tabName === activeTab.value || loading.value) return if (!tabName || tabName === activeTab.value || loading.value) return
backendGroupedLoading.value = true
activeTab.value = tabName activeTab.value = tabName
} }
@@ -4887,7 +4930,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
background: rgba(246, 247, 249, 0.82); background: #f6f7f9;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
pointer-events: all; pointer-events: all;
} }