package handler import ( "context" "fmt" "strings" "time" "github.com/go-telegram/bot/models" "worktimeBot/internal/domain" ) // -- Command -- func (h *Handler) handleBurnout(ctx context.Context, msg *models.Message, user *domain.User) { text := h.buildBurnoutText(user) h.sendWithKB(ctx, msg.Chat.ID, text, backKeyboard()) } // -- Callback -- func (h *Handler) burnoutCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) { defer h.answerCb(ctx, cbID) text := h.buildBurnoutText(user) kb := backKeyboard() h.editText(ctx, chatID, msgID, text, &kb) } // -- Assessment -- func (h *Handler) buildBurnoutText(user *domain.User) string { res, err := h.assessBurnout(user) if err != nil { return "Burnout assessment unavailable." } var b strings.Builder b.WriteString(fmt.Sprintf("Burnout Assessment\nLevel: %s (%d/100)\n\n", res.Level, res.Score)) if res.ConsecutiveDays > 0 { b.WriteString(fmt.Sprintf(" Consecutive workdays: %d days (%d pts)\n", res.ConsecutiveDays, res.ConsecutivePts)) } if res.WorkSeconds > 0 { wh := float64(res.WorkSeconds) / domain.SecondsPerHour b.WriteString(fmt.Sprintf(" Today's work: %.1f hours (%d pts)\n", wh, res.LongHoursPts)) } if res.BreakSeconds > 0 { br := float64(res.BreakSeconds) total := float64(res.WorkSeconds + res.BreakSeconds) ratio := br / total * 100 if ratio > 100 { ratio = 100 } b.WriteString(fmt.Sprintf(" Break ratio: %.0f%% (%d pts)\n", ratio, res.BreakRatioPts)) } if res.WeekendPts > 0 { b.WriteString(fmt.Sprintf(" Weekend work: yes (%d pts)\n", res.WeekendPts)) } b.WriteString(fmt.Sprintf(" Work trend: %s (%d pts)\n", res.TrendDirection, res.TrendPts)) if res.LateNightPts > 0 { b.WriteString(fmt.Sprintf(" Late-night work: yes (%d pts)\n", res.LateNightPts)) } b.WriteString("\nRecommendations:\n") switch res.Level { case domain.BurnoutHealthy: b.WriteString(" Keep up the balanced routine!") case domain.BurnoutMild: b.WriteString(" Consider taking short breaks throughout the day.") b.WriteString("\n Make sure to disconnect after work hours.") case domain.BurnoutModerate: b.WriteString(" Consider taking a day off soon.") b.WriteString("\n Review your workload distribution.") b.WriteString("\n Ensure you are taking adequate breaks.") case domain.BurnoutHigh: b.WriteString(" Strongly consider taking time off.") b.WriteString("\n Your workload patterns indicate potential strain.") b.WriteString("\n Please prioritize rest and recovery.") } return b.String() } func (h *Handler) assessBurnout(user *domain.User) (*domain.BurnoutResult, error) { loc := h.loadLocation(user.Timezone) now := time.Now().In(loc) today := now.Format(domain.DateLayout) result := &domain.BurnoutResult{} stats, err := h.Repo.GetOrCreateRPGStats(user.ID) if err == nil { result.ConsecutiveDays = stats.CurrentStreak } switch { case result.ConsecutiveDays >= 7: result.ConsecutivePts = 25 case result.ConsecutiveDays >= 4: result.ConsecutivePts = 15 case result.ConsecutiveDays >= 1: result.ConsecutivePts = 5 } todayEvents, _ := h.Repo.EventsForDayByDate(user.ID, today) if len(todayEvents) > 0 { day, _ := h.Repo.GetDay(user.ID, today) if day != nil { y, m, d := now.Date() dayStart := time.Date(y, m, d, 0, 0, 0, 0, loc).Unix() totals := domain.ComputeDailyTotals(todayEvents, day.MinBreakThreshold, dayStart, now.Unix()) result.WorkSeconds = totals.TotalSeconds result.BreakSeconds = totals.BreakSeconds workHours := float64(totals.TotalSeconds) / domain.SecondsPerHour switch { case workHours >= 12: result.LongHoursPts = 20 case workHours >= 10: result.LongHoursPts = 15 case workHours >= 8: result.LongHoursPts = 10 } total := totals.TotalSeconds + totals.BreakSeconds if total > 0 { breakRatio := float64(totals.BreakSeconds) / float64(total) * 100 switch { case breakRatio < 5: result.BreakRatioPts = 15 case breakRatio < 10: result.BreakRatioPts = 10 case breakRatio < 20: result.BreakRatioPts = 5 } } if len(todayEvents) > 0 { lastEvent := todayEvents[len(todayEvents)-1] lt := time.Unix(lastEvent.OccurredAt, 0).In(loc) minutes := lt.Hour()*60 + lt.Minute() switch { case minutes >= 22*60: result.LateNightPts = 10 case minutes >= 20*60: result.LateNightPts = 5 } } } } weekday := now.Weekday() if weekday == time.Saturday || weekday == time.Sunday { if result.WorkSeconds > 0 { result.WeekendPts = 15 } } result.TrendPts, result.TrendDirection = h.computeTrend(user.ID, today, loc) result.Score = result.ConsecutivePts + result.LongHoursPts + result.BreakRatioPts + result.WeekendPts + result.TrendPts + result.LateNightPts switch { case result.Score >= 61: result.Level = domain.BurnoutHigh case result.Score >= 41: result.Level = domain.BurnoutModerate case result.Score >= 21: result.Level = domain.BurnoutMild default: result.Level = domain.BurnoutHealthy } return result, nil } func (h *Handler) computeTrend(userID int64, today string, loc *time.Location) (int, string) { t, err := time.Parse(domain.DateLayout, today) if err != nil { return 0, "unknown" } var recent, older [3]int64 for i := 0; i < 3; i++ { d := t.AddDate(0, 0, -(i + 1)).Format(domain.DateLayout) recent[2-i] = h.dayWorkSeconds(userID, d, loc) } for i := 0; i < 3; i++ { d := t.AddDate(0, 0, -(i + 4)).Format(domain.DateLayout) older[2-i] = h.dayWorkSeconds(userID, d, loc) } var recentAvg, olderAvg float64 for _, v := range recent { recentAvg += float64(v) } for _, v := range older { olderAvg += float64(v) } recentAvg /= 3.0 olderAvg /= 3.0 return domain.ComputeTrendFromAvgs(recentAvg, olderAvg) } func (h *Handler) dayWorkSeconds(userID int64, date string, loc *time.Location) int64 { events, err := h.Repo.EventsForDayByDate(userID, date) if err != nil || len(events) == 0 { return 0 } day, err := h.Repo.GetDay(userID, date) if err != nil || day == nil { return 0 } y, m, d := domain.ParseGregorianDateKey(date) dayStart := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc).Unix() dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix() totals := domain.ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd) return totals.TotalSeconds }