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:
2026-06-25 09:54:22 +03:30
parent 3835b002a7
commit d15ed46066
45 changed files with 4947 additions and 5430 deletions

111
internal/handler/league.go Normal file
View File

@@ -0,0 +1,111 @@
package handler
import (
"context"
"fmt"
"strings"
"github.com/go-telegram/bot/models"
"worktimeBot/internal/domain"
)
// -- Command --
func (h *Handler) handleLeague(ctx context.Context, msg *models.Message, user *domain.User) {
st, err := h.Repo.GetOrCreateSettings(user.ID)
if err != nil {
h.sendText(ctx, msg.Chat.ID, "Error loading settings")
return
}
if !st.LeagueOptIn {
h.sendText(ctx, msg.Chat.ID, "League participation is disabled.\nEnable it in Settings or use /leaguetoggle.")
return
}
text := h.buildLeagueText(domain.LeagueSortXP)
kb := h.buildLeagueKeyboard(string(domain.LeagueSortXP))
h.sendWithKB(ctx, msg.Chat.ID, text, kb)
}
// -- Callback --
func (h *Handler) leagueCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User, orderBy string) {
defer h.answerCb(ctx, cbID)
st, err := h.Repo.GetOrCreateSettings(user.ID)
if err != nil {
return
}
if !st.LeagueOptIn {
kb := backKeyboard()
h.editText(ctx, chatID, msgID, "League participation is disabled.\nEnable it in Settings to view rankings.", &kb)
return
}
text := h.buildLeagueText(domain.LeagueSortOption(orderBy))
kb := h.buildLeagueKeyboard(orderBy)
h.editText(ctx, chatID, msgID, text, &kb)
}
// -- Builders --
func (h *Handler) buildLeagueText(orderBy domain.LeagueSortOption) string {
entries, err := h.Repo.GetLeagueRankings(string(orderBy))
if err != nil {
return "League rankings unavailable."
}
if len(entries) == 0 {
return "No participants in the league yet.\nEnable League participation in Settings to join!"
}
var b strings.Builder
orderLabel := map[string]string{"xp": "XP", "level": "Level", "streak": "Streak", "hours": "Hours"}
b.WriteString(fmt.Sprintf("WorkTime League - %s\n\n", orderLabel[string(orderBy)]))
for i, e := range entries {
rank := i + 1
name := e.Username
if name == "" {
name = "Anonymous"
}
b.WriteString(fmt.Sprintf("%d %s\n", rank, name))
b.WriteString(fmt.Sprintf(" XP:%d Lv:%d S:%d %.1fh\n", e.XP, e.Level, e.Streak, e.TotalHours))
}
b.WriteString(fmt.Sprintf("\n%d participant(s)\n", len(entries)))
return b.String()
}
func (h *Handler) buildLeagueRankText(userID int64) string {
entries, err := h.Repo.GetLeagueRankings("xp")
if err != nil || len(entries) == 0 {
return ""
}
for i, e := range entries {
if e.UserID == userID {
return fmt.Sprintf("League rank: #%d of %d", i+1, len(entries))
}
}
return ""
}
func (h *Handler) buildLeagueKeyboard(currentOrder string) models.InlineKeyboardMarkup {
type sortBtn struct{ label, data string }
orders := []sortBtn{
{"XP", "league_xp"},
{"Level", "league_level"},
{"Streak", "league_streak"},
{"Hours", "league_hours"},
}
rows := [][]models.InlineKeyboardButton{}
for i := 0; i < len(orders); i += 2 {
row := []models.InlineKeyboardButton{}
for j := i; j < i+2 && j < len(orders); j++ {
label := orders[j].label
if orders[j].data == "league_"+currentOrder {
label = "> " + label
}
row = append(row, models.InlineKeyboardButton{Text: label, CallbackData: orders[j].data})
}
rows = append(rows, row)
}
rows = append(rows, []models.InlineKeyboardButton{
{Text: "Back to Menu", CallbackData: "back_menu"},
})
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
}