fix: resolve go vet error and dayStart logic in ComputeDailyTotals
- Fix 'string(d)' int-to-rune conversion in rpg_test.go by using fmt.Sprintf - Fix ComputeDailyTotals dayStart logic: initial state should be StateWorking when dayStart > 0, correctly counting work from dayStart to first out event - Update README project structure to reflect new domain/handler/repo layout - Implement missing achievements: rpg_pioneer (on RPG enable) and salary_setter (on first rate set) - Add tryUnlockAchievement helper to Handler
This commit is contained in:
207
internal/handler/burnout.go
Normal file
207
internal/handler/burnout.go
Normal file
@@ -0,0 +1,207 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user