From 4fcb69441802ce7f7ffcddede7466c8d99967424 Mon Sep 17 00:00:00 2001 From: db123 Date: Thu, 25 Jun 2026 02:08:33 +0330 Subject: [PATCH] fix: compute work hours from events at runtime; add Achievements button to RPG view --- internal/bot/clock.go | 3 +- internal/bot/handlers.go | 6 ++- internal/bot/rpg.go | 104 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 104 insertions(+), 9 deletions(-) diff --git a/internal/bot/clock.go b/internal/bot/clock.go index 6879425..9798cfa 100644 --- a/internal/bot/clock.go +++ b/internal/bot/clock.go @@ -73,7 +73,8 @@ func (h *Handler) clockOut(chatID int64) (string, error) { streak, _ := h.updateStreak(user.ID, today, true) xp, leveledUp, newLevel, _ := h.computeAndAwardXP(user.ID, sessionWork, streak, today) stats, _ := h.DB.GetOrCreateRPGStats(user.ID) - unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend) + totalHours := float64(h.totalWorkSeconds(user.ID, loc)) / secondsPerHour + unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend, totalHours) text += fmt.Sprintf("\n\n+%d XP (Lv.%d)", xp, newLevel) if leveledUp { diff --git a/internal/bot/handlers.go b/internal/bot/handlers.go index 2a637b9..6fd671d 100644 --- a/internal/bot/handlers.go +++ b/internal/bot/handlers.go @@ -198,7 +198,7 @@ func (h *Handler) HandleMessage(ctx context.Context, msg *models.Message) { case "/setbreak": h.handleSetBreakMsg(ctx, msg) case "/rpg": - h.handleActionMsg(ctx, msg, h.rpgStatus) + h.handleRPGCmd(ctx, msg) case "/achievements": h.handleActionMsg(ctx, msg, h.achievementsList) case "/salary": @@ -298,6 +298,10 @@ func (h *Handler) HandleCallback(ctx context.Context, cb *models.CallbackQuery) h.leagueToggleCallback(ctx, chatID, msgID, cb.ID) case "reporttoggle": h.reportToggleCallback(ctx, chatID, msgID, cb.ID) + case "rpg_achievements": + h.rpgAchievementsCallback(ctx, chatID, msgID, cb.ID) + case "rpg_back": + h.rpgBackCallback(ctx, chatID, msgID, cb.ID) case "username": h.usernameCallback(ctx, chatID, msgID, cb.ID) case "currency": diff --git a/internal/bot/rpg.go b/internal/bot/rpg.go index ad4efa0..6fb693d 100644 --- a/internal/bot/rpg.go +++ b/internal/bot/rpg.go @@ -1,11 +1,14 @@ package bot import ( + "context" "fmt" "log/slog" "strings" "time" + "github.com/go-telegram/bot/models" + "worktimeBot/internal/db" ) @@ -121,12 +124,12 @@ func (h *Handler) updateStreak(userID int64, date string, hasWorkToday bool) (in } // checkAchievements evaluates and unlocks any newly earned achievements. -func (h *Handler) checkAchievements(userID int64, stats *db.RPGStats, workSeconds int64, date string, isWeekend bool) []string { +func (h *Handler) checkAchievements(userID int64, stats *db.RPGStats, workSeconds int64, date string, isWeekend bool, totalHours float64) []string { unlocker := newAchievementUnlocker(h, userID) h.checkEarlyAchievements(unlocker, workSeconds) h.checkEventTimeAchievements(unlocker, userID, date) h.checkStreakAchievements(unlocker, stats) - h.checkHourAchievements(unlocker, stats) + h.checkHourAchievements(unlocker, stats, totalHours) h.checkSessionAchievements(unlocker, workSeconds, isWeekend) h.checkLevelAchievements(unlocker, stats) return unlocker.unlocked @@ -201,8 +204,7 @@ func (h *Handler) checkStreakAchievements(u *achievementUnlocker, stats *db.RPGS } } -func (h *Handler) checkHourAchievements(u *achievementUnlocker, stats *db.RPGStats) { - hoursWorked := float64(stats.TotalWorkSeconds) / secondsPerHour +func (h *Handler) checkHourAchievements(u *achievementUnlocker, stats *db.RPGStats, hoursWorked float64) { if hoursWorked >= 100.0 { u.unlock("hundred_hours") } @@ -235,8 +237,34 @@ func (h *Handler) checkLevelAchievements(u *achievementUnlocker, stats *db.RPGSt } } +// totalWorkSeconds computes actual total work seconds from all events for the user. +// Calculated at runtime to stay in sync with event edits. +func (h *Handler) totalWorkSeconds(userID int64, loc *time.Location) int64 { + days, err := h.DB.GetUserDaysInRange(userID, "2000-01-01", "2100-01-01") + if err != nil { + return 0 + } + + var total int64 + for _, day := range days { + if day.IsDayOff { + continue + } + events, err := h.DB.EventsForDayByDayID(day.ID) + if err != nil || len(events) == 0 { + continue + } + y, m, d := parseGregorianDateKey(day.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 := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd) + total += totals.TotalSeconds + } + return total +} + // buildRPGStatus returns a formatted string showing the user's RPG stats. -func (h *Handler) buildRPGStatus(userID int64) string { +func (h *Handler) buildRPGStatus(userID int64, loc *time.Location) string { stats, err := h.DB.GetOrCreateRPGStats(userID) if err != nil { return "RPG stats unavailable" @@ -252,7 +280,7 @@ func (h *Handler) buildRPGStatus(userID int64) string { b.WriteString(fmt.Sprintf("XP: %d / %d (%d%%)\n", stats.XP, nextLevelXP, xpIntoLevel*100/xpNeeded)) b.WriteString(fmt.Sprintf("Streak: %d days (best: %d)\n", stats.CurrentStreak, stats.LongestStreak)) - hoursTotal := float64(stats.TotalWorkSeconds) / secondsPerHour + hoursTotal := float64(h.totalWorkSeconds(userID, loc)) / secondsPerHour b.WriteString(fmt.Sprintf("Total work: %.1f hours\n", hoursTotal)) // Show achievements count @@ -296,7 +324,69 @@ func (h *Handler) rpgStatus(chatID int64) (string, error) { if !st.RPGEnabled { return "RPG system is disabled. Enable it in Settings or use /rpgtoggle.", nil } - return h.buildRPGStatus(user.ID), nil + loc := loadLocation(user.Timezone) + return h.buildRPGStatus(user.ID, loc), nil +} + +// handleRPGCmd handles /rpg — shows stats with an Achievements button. +func (h *Handler) handleRPGCmd(ctx context.Context, msg *models.Message) { + user, err := h.getOrCreateUser(msg.Chat.ID) + if err != nil { + h.sendText(ctx, msg.Chat.ID, "Error loading profile") + return + } + st, err := h.DB.GetOrCreateSettings(user.ID) + if err != nil { + h.sendText(ctx, msg.Chat.ID, "Error loading settings") + return + } + if !st.RPGEnabled { + h.sendText(ctx, msg.Chat.ID, "RPG system is disabled. Enable it in Settings or use /rpgtoggle.") + return + } + loc := loadLocation(user.Timezone) + text := h.buildRPGStatus(user.ID, loc) + kb := models.InlineKeyboardMarkup{ + InlineKeyboard: [][]models.InlineKeyboardButton{ + {{Text: "Achievements", CallbackData: "rpg_achievements"}}, + {{Text: "Back to Menu", CallbackData: "back_menu"}}, + }, + } + h.sendWithKB(ctx, msg.Chat.ID, text, kb) +} + +// rpgAchievementsCallback shows achievements from the RPG view. +func (h *Handler) rpgAchievementsCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { + defer h.answerCb(ctx, callbackID) + user, err := h.getOrCreateUser(chatID) + if err != nil { + return + } + text := h.buildAchievementsList(user.ID) + kb := models.InlineKeyboardMarkup{ + InlineKeyboard: [][]models.InlineKeyboardButton{ + {{Text: "Back to RPG", CallbackData: "rpg_back"}}, + }, + } + h.editText(ctx, chatID, msgID, text, &kb) +} + +// rpgBackCallback returns from achievements to the RPG stats view. +func (h *Handler) rpgBackCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { + defer h.answerCb(ctx, callbackID) + user, err := h.getOrCreateUser(chatID) + if err != nil { + return + } + loc := loadLocation(user.Timezone) + text := h.buildRPGStatus(user.ID, loc) + kb := models.InlineKeyboardMarkup{ + InlineKeyboard: [][]models.InlineKeyboardButton{ + {{Text: "Achievements", CallbackData: "rpg_achievements"}}, + {{Text: "Back to Menu", CallbackData: "back_menu"}}, + }, + } + h.editText(ctx, chatID, msgID, text, &kb) } // achievementsList returns the user's unlocked achievements.