From 8d0f3c302a557e614a27f7f68945ac6e8ded23e3 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 2 Jul 2026 20:07:14 +0300 Subject: [PATCH] ui: update ProductPerformanceProfitability page to enhance table views and tabs management --- svc/cmd/product_performance_refresh/main.go | 83 ---------- svc/main.go | 5 - svc/product_performance_scheduler.go | 159 ++------------------ svc/routes/product_performance.go | 55 ------- 4 files changed, 15 insertions(+), 287 deletions(-) delete mode 100644 svc/cmd/product_performance_refresh/main.go diff --git a/svc/cmd/product_performance_refresh/main.go b/svc/cmd/product_performance_refresh/main.go deleted file mode 100644 index 3f4829e..0000000 --- a/svc/cmd/product_performance_refresh/main.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "bssapp-backend/db" - "bssapp-backend/queries" - "context" - "encoding/json" - "flag" - "fmt" - "log" - "os" - "runtime" - "strconv" - "time" - - "github.com/joho/godotenv" -) - -func main() { - mode := flag.String("mode", "full", "refresh mode: full or delta") - stage := flag.String("stage", "all", "refresh stage: all, sales, stock, price, or kpi") - resumeAfter := flag.Int("resume-after", 0, "skip this many source rows before inserting; implies skip-delete") - skipDelete := flag.Bool("skip-delete", false, "skip deleting cache rows for the selected stage") - productPrefix := flag.String("product-prefix", "", "optional product code prefix filter: S, O, N, X, I, or A") - start := flag.String("start", "2022-01-01", "start date in YYYY-MM-DD") - end := flag.String("end", "", "end date in YYYY-MM-DD; default today") - mssqlTimeoutSec := flag.Int("mssql-timeout-sec", 43200, "MSSQL connection/read timeout in seconds for long refresh jobs") - flag.Parse() - - _ = godotenv.Load(".env", "mail.env") - if runtime.GOOS == "windows" { - _ = godotenv.Overload(".env.local") - } - if *mssqlTimeoutSec > 0 { - timeout := strconv.Itoa(*mssqlTimeoutSec) - _ = os.Setenv("MSSQL_CONNECTION_TIMEOUT_SEC", timeout) - _ = os.Setenv("MSSQL_DIAL_TIMEOUT_SEC", timeout) - } - - if err := db.ConnectMSSQL(); err != nil { - log.Fatalf("mssql connect failed: %v", err) - } - defer db.MssqlDB.Close() - - pg, err := db.ConnectPostgres() - if err != nil { - log.Fatalf("postgres connect failed: %v", err) - } - defer pg.Close() - - startDate, err := time.Parse("2006-01-02", *start) - if err != nil { - log.Fatalf("invalid start date: %v", err) - } - var endDate time.Time - if *end == "" { - endDate = time.Now() - } else { - endDate, err = time.Parse("2006-01-02", *end) - if err != nil { - log.Fatalf("invalid end date: %v", err) - } - } - - refreshTimeout := time.Duration(*mssqlTimeoutSec) * time.Second - ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout) - defer cancel() - - result, err := queries.RefreshProductPerformance(ctx, pg, queries.ProductPerformanceRefreshRequest{ - Mode: *mode, - Stage: *stage, - ResumeAfter: *resumeAfter, - SkipDelete: *skipDelete, - ProductPrefix: *productPrefix, - StartDate: startDate, - EndDate: endDate, - }) - if err != nil { - log.Fatalf("refresh failed: %v", err) - } - out, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(out)) -} diff --git a/svc/main.go b/svc/main.go index c5a3049..d4d9510 100644 --- a/svc/main.go +++ b/svc/main.go @@ -989,11 +989,6 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "pricing", "view", wrapV3(routes.GetProductPerformanceStockSizesHandler()), ) - bindV3(r, pgDB, - "/api/pricing/product-performance/refresh", "POST", - "pricing", "update", - wrapV3(routes.PostProductPerformanceRefreshHandler(pgDB)), - ) bindV3(r, pgDB, "/api/pricing/product-series/definitions", "GET", "pricing", "view", diff --git a/svc/product_performance_scheduler.go b/svc/product_performance_scheduler.go index 387b169..673815b 100644 --- a/svc/product_performance_scheduler.go +++ b/svc/product_performance_scheduler.go @@ -25,28 +25,26 @@ func startProductPerformanceScheduler(pgDB *sql.DB) { deltaDays := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_DAYS", 45, 1, 365) deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00") deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24) - - fullEnabled := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_ENABLED", true) - fullWeekday := productPerformanceEnvWeekday("PRODUCT_PERFORMANCE_FULL_WEEKDAY", time.Saturday) - fullHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_FULL_HHMM", "16:00") - fullTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_FULL_TIMEOUT_HOURS", 18, 1, 48) - fullStart := productPerformanceEnvDate("PRODUCT_PERFORMANCE_FULL_START_DATE", time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local)) - - resumeOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_RESUME_ON_STARTUP", true) - runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_RUN_ON_STARTUP", false) + runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false) var running int32 - runRefresh := func(reason string, req queries.ProductPerformanceRefreshRequest, timeoutHours int) { + runDelta := func(reason string) { if !atomic.CompareAndSwapInt32(&running, 0, 1) { log.Printf("[ProductPerformanceJob] skip (%s): already running", reason) return } defer atomic.StoreInt32(&running, 0) - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutHours)*time.Hour) + endDate := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deltaTimeoutHours)*time.Hour) defer cancel() - result, err := queries.RefreshProductPerformance(ctx, pgDB, req) + result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{ + Mode: "delta", + Stage: "all", + StartDate: endDate.AddDate(0, 0, -deltaDays), + EndDate: endDate, + }) if err != nil { log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err) return @@ -55,105 +53,22 @@ func startProductPerformanceScheduler(pgDB *sql.DB) { reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.DurationMS) } - runDelta := func(reason string) { - endDate := time.Now() - runRefresh(reason, queries.ProductPerformanceRefreshRequest{ - Mode: "delta", - Stage: "all", - StartDate: endDate.AddDate(0, 0, -deltaDays), - EndDate: endDate, - }, deltaTimeoutHours) - } - - runFull := func(reason string) { - runRefresh(reason, queries.ProductPerformanceRefreshRequest{ - Mode: "full", - Stage: "all", - StartDate: fullStart, - EndDate: time.Now(), - }, fullTimeoutHours) - } - - runFullResume := func(reason string) { - resumeAfter, ok := productPerformanceStockResumeAfter(pgDB, fullStart, time.Now()) - if !ok { - log.Printf("[ProductPerformanceJob] resume skip (%s): no committed full stock progress found", reason) - return - } - runRefresh(reason, queries.ProductPerformanceRefreshRequest{ - Mode: "full", - Stage: "stock", - ResumeAfter: resumeAfter, - SkipDelete: true, - StartDate: fullStart, - EndDate: time.Now(), - }, fullTimeoutHours) - } - - log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d weekly_full=%t %s %s full_start=%s resume_on_startup=%t", - deltaHHMM, deltaDays, fullEnabled, fullWeekday.String(), fullHHMM, fullStart.Format("2006-01-02"), resumeOnStartup) + log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t", + deltaHHMM, deltaDays, runOnStartup) go func() { - if resumeOnStartup { + if runOnStartup { time.Sleep(20 * time.Second) - runFullResume("startup-resume") - } else if runOnStartup { - time.Sleep(20 * time.Second) - runDelta("startup") + runDelta("startup-delta") } for { next := productPerformanceNextDaily(time.Now(), deltaHHMM) log.Printf("[ProductPerformanceJob] daily delta next_at=%s in=%s", next.Format(time.RFC3339), time.Until(next).Round(time.Second)) time.Sleep(time.Until(next)) - runDelta("daily-02") + runDelta("daily-delta") } }() - - if fullEnabled { - go func() { - 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 productPerformanceStockResumeAfter(pgDB *sql.DB, startDate, endDate time.Time) (int, bool) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - if err := queries.EnsureProductPerformanceTables(pgDB); err != nil { - log.Printf("[ProductPerformanceJob] resume inspect failed: %v", err) - return 0, false - } - var stockRows int - var maxStockDate sql.NullTime - var maxKPIDate sql.NullTime - if err := pgDB.QueryRowContext(ctx, ` -SELECT COUNT(*), MAX(stock_date) -FROM mk_product_performance_stock_daily -WHERE stock_date BETWEEN $1 AND $2 -`, startDate, endDate).Scan(&stockRows, &maxStockDate); err != nil { - log.Printf("[ProductPerformanceJob] resume inspect stock failed: %v", err) - return 0, false - } - if stockRows <= 0 { - return 0, false - } - if err := pgDB.QueryRowContext(ctx, ` -SELECT MAX(kpi_date) -FROM mk_product_performance_kpi_daily -`).Scan(&maxKPIDate); err != nil { - log.Printf("[ProductPerformanceJob] resume inspect kpi failed: %v", err) - return 0, false - } - if maxStockDate.Valid && maxKPIDate.Valid && !maxStockDate.Time.After(maxKPIDate.Time) { - return 0, false - } - return stockRows, true } func productPerformanceNextDaily(now time.Time, hhmm string) time.Time { @@ -165,17 +80,6 @@ func productPerformanceNextDaily(now time.Time, hhmm string) time.Time { return next } -func productPerformanceNextWeekly(now time.Time, weekday time.Weekday, hhmm string) time.Time { - hour, minute := productPerformanceParseHHMM(hhmm, 16, 0) - next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()) - days := (int(weekday) - int(now.Weekday()) + 7) % 7 - next = next.AddDate(0, 0, days) - if !next.After(now) { - next = next.AddDate(0, 0, 7) - } - return next -} - func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) { parts := strings.Split(strings.TrimSpace(raw), ":") if len(parts) != 2 { @@ -216,36 +120,3 @@ func productPerformanceEnvBool(name string, fallback bool) bool { } 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 - } - t, err := time.ParseInLocation("2006-01-02", raw, time.Local) - if err != nil { - return fallback - } - return t -} - -func productPerformanceEnvWeekday(name string, fallback time.Weekday) time.Weekday { - switch strings.ToLower(strings.TrimSpace(os.Getenv(name))) { - case "0", "sunday", "pazar": - return time.Sunday - case "1", "monday", "pazartesi": - return time.Monday - case "2", "tuesday", "sali", "salı": - return time.Tuesday - case "3", "wednesday", "carsamba", "çarşamba": - return time.Wednesday - case "4", "thursday", "persembe", "perşembe": - return time.Thursday - case "5", "friday", "cuma": - return time.Friday - case "6", "saturday", "cumartesi": - return time.Saturday - default: - return fallback - } -} diff --git a/svc/routes/product_performance.go b/svc/routes/product_performance.go index 36afee0..55bc6de 100644 --- a/svc/routes/product_performance.go +++ b/svc/routes/product_performance.go @@ -240,49 +240,6 @@ func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc { } } -type productPerformanceRefreshPayload struct { - Mode string `json:"mode"` - Stage string `json:"stage"` - ResumeAfter int `json:"resume_after"` - SkipDelete bool `json:"skip_delete"` - ProductPrefix string `json:"product_prefix"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` -} - -func PostProductPerformanceRefreshHandler(pg *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - var payload productPerformanceRefreshPayload - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - http.Error(w, "invalid payload", http.StatusBadRequest) - return - } - now := time.Now() - startDate := parseDateOrZero(payload.StartDate) - endDate := parseDateOrZero(payload.EndDate) - if endDate.IsZero() { - endDate = now - } - ctx, cancel := context.WithTimeout(r.Context(), 4*time.Hour) - defer cancel() - result, err := queries.RefreshProductPerformance(ctx, pg, queries.ProductPerformanceRefreshRequest{ - Mode: payload.Mode, - Stage: payload.Stage, - ResumeAfter: payload.ResumeAfter, - SkipDelete: payload.SkipDelete, - ProductPrefix: payload.ProductPrefix, - StartDate: startDate, - EndDate: endDate, - }) - if err != nil { - http.Error(w, "urun performans refresh hatasi: "+err.Error(), http.StatusInternalServerError) - return - } - _ = json.NewEncoder(w).Encode(result) - } -} - func intQuery(r *http.Request, key string, fallback int) int { raw := strings.TrimSpace(r.URL.Query().Get(key)) if raw == "" { @@ -302,15 +259,3 @@ func boolQuery(r *http.Request, key string, fallback bool) bool { } return raw == "1" || raw == "true" || raw == "yes" || raw == "on" } - -func parseDateOrZero(raw string) time.Time { - raw = strings.TrimSpace(raw) - if raw == "" { - return time.Time{} - } - t, err := time.Parse("2006-01-02", raw) - if err != nil { - return time.Time{} - } - return t -}