refactor: break up large functions, add RPG/league/salary/burnout, fix bugs, add tests
- Refactor GenerateMonthlyReport (198→70), HandleCallback (128→85), handleHistoryCallback (131→38), handleExportCallback (100→25), checkAchievements (86→25) into extracted helper functions - Add RPG system (XP, levels, streak, achievements, burnout) - Add WorkTime League leaderboard - Add salary estimation with configurable currency/rate - Add input sanitization (SanitizeNote, SanitizeDisplayName) - Add settings select-style pickers for toggles (report, RPG, league) - Add break threshold inline picker UI - Add event notes with pending state and /note command - Add multi-calendar support (Jalali, Hijri) - Add Excel export with theme picker - Fix: getTodayBreakThreshold uses user's timezone (was UTC) - Fix: acknowledgeCallback passes real callback ID - Fix: currency symbol safety with currencySymbol() helper - Fix: count work hours in summary on day-off days - Fix: unsilence all error returns from SQL Exec/Bot API/migrations - Remove stale db/schema.sql and unreferenced computeWeekTotals - Extract constants: DateLayout, TimeLayout, secondsPerHour, etc. - Add 12 new tests (XP, salary, sanitize, currency, dateutil) - Remove duplicate package doc comment in totals.go
This commit is contained in:
157
internal/bot/league.go
Normal file
157
internal/bot/league.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
)
|
||||
|
||||
// LeagueSortOption represents a valid league sort column.
|
||||
type LeagueSortOption string
|
||||
|
||||
const (
|
||||
LeagueSortXP LeagueSortOption = "xp"
|
||||
LeagueSortLevel LeagueSortOption = "level"
|
||||
LeagueSortStreak LeagueSortOption = "streak"
|
||||
LeagueSortHours LeagueSortOption = "hours"
|
||||
)
|
||||
|
||||
// buildLeagueText returns the formatted league rankings.
|
||||
func (h *Handler) buildLeagueText(orderBy LeagueSortOption) string {
|
||||
entries, err := h.DB.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": "Total Hours",
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("WorkTime League — sorted by %s\n\n", orderLabel[string(orderBy)]))
|
||||
|
||||
// Determine column widths
|
||||
maxUserLen := 0
|
||||
for _, e := range entries {
|
||||
l := len(e.Username)
|
||||
if l > maxUserLen {
|
||||
maxUserLen = l
|
||||
}
|
||||
}
|
||||
if maxUserLen < 8 {
|
||||
maxUserLen = 8
|
||||
}
|
||||
if maxUserLen > 20 {
|
||||
maxUserLen = 20
|
||||
}
|
||||
|
||||
header := fmt.Sprintf("%-3s %-*s %8s %6s %6s %12s", "#", maxUserLen, "Username", "XP", "Level", "Streak", "Hours")
|
||||
b.WriteString(header + "\n")
|
||||
b.WriteString(strings.Repeat("-", len(header)) + "\n")
|
||||
|
||||
for i, e := range entries {
|
||||
rank := i + 1
|
||||
name := e.Username
|
||||
if len(name) > maxUserLen {
|
||||
name = name[:maxUserLen]
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%-3d %-*s %8d %6d %6d %12.1f\n",
|
||||
rank, maxUserLen, name, e.XP, e.Level, e.Streak, e.TotalHours))
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("\n%d participant(s)\n", len(entries)))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// buildLeagueRankText returns the user's personal rank in the league.
|
||||
func (h *Handler) buildLeagueRankText(userID int64) string {
|
||||
entries, err := h.DB.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 ""
|
||||
}
|
||||
|
||||
// handleLeagueMsg handles the /league command.
|
||||
func (h *Handler) handleLeagueMsg(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.LeagueOptIn {
|
||||
h.sendText(ctx, msg.Chat.ID, "League participation is disabled.\nEnable it in Settings or use /leaguetoggle.")
|
||||
return
|
||||
}
|
||||
text := h.buildLeagueText(LeagueSortXP)
|
||||
kb := h.buildLeagueKeyboard("xp")
|
||||
h.sendWithKB(ctx, msg.Chat.ID, text, kb)
|
||||
}
|
||||
|
||||
// handleLeagueCallback handles league inline view.
|
||||
func (h *Handler) handleLeagueCallback(ctx context.Context, chatID int64, msgID int, callbackID, orderBy string) {
|
||||
defer h.answerCb(ctx, callbackID)
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
st, err := h.DB.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(LeagueSortOption(orderBy))
|
||||
kb := h.buildLeagueKeyboard(orderBy)
|
||||
h.editText(ctx, chatID, msgID, text, &kb)
|
||||
}
|
||||
|
||||
// buildLeagueKeyboard creates the league navigation keyboard.
|
||||
func (h *Handler) buildLeagueKeyboard(currentOrder string) models.InlineKeyboardMarkup {
|
||||
orders := []struct {
|
||||
label string
|
||||
data string
|
||||
}{
|
||||
{"XP", "league_xp"},
|
||||
{"Level", "league_level"},
|
||||
{"Streak", "league_streak"},
|
||||
{"Hours", "league_hours"},
|
||||
}
|
||||
|
||||
rows := [][]models.InlineKeyboardButton{}
|
||||
for _, o := range orders {
|
||||
label := o.label
|
||||
if o.data == "league_"+currentOrder {
|
||||
label = "> " + label
|
||||
}
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: label, CallbackData: o.data},
|
||||
})
|
||||
}
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
})
|
||||
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
||||
}
|
||||
Reference in New Issue
Block a user