ui: update ProductPerformanceProfitability page to enhance table views and tabs management

This commit is contained in:
M_Kececi
2026-07-02 20:07:14 +03:00
parent 2fc9699ff1
commit 8d0f3c302a
4 changed files with 15 additions and 287 deletions
+15 -144
View File
@@ -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
}
}