Files
worktimeBot/internal/bot/clock.go
db123 344c615666 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
2026-06-25 01:02:16 +03:30

140 lines
3.7 KiB
Go

package bot
import (
"database/sql"
"errors"
"fmt"
"time"
)
// clockIn records a clock-in event for today.
// Returns the formatted time or an error if already clocked in today.
func (h *Handler) clockIn(chatID int64) (string, error) {
user, day, err := h.getUserToday(chatID)
if err != nil {
return "", err
}
last, err := h.DB.GetLastEvent(user.ID)
if err != nil && err != sql.ErrNoRows {
return "", err
}
if last != nil && last.EventType == "in" {
loc := loadLocation(user.Timezone)
now := time.Now().In(loc)
lastTime := time.Unix(last.OccurredAt, 0).In(loc)
if lastTime.Format(DateLayout) == now.Format(DateLayout) {
return "", errors.New("already clocked in")
}
}
loc := loadLocation(user.Timezone)
now := time.Now().In(loc)
wt := day.CurrentWorkTypeID
if err := h.DB.CreateEvent(user.ID, day.ID, "in", &wt, now.Unix(), ""); err != nil {
return "", err
}
return fmt.Sprintf("clocked in at %s", now.Format(TimeLayout)), nil
}
// clockOut records a clock-out event for today.
// Returns the formatted time or an error if not clocked in.
// Also awards XP and updates streaks if RPG is enabled.
func (h *Handler) clockOut(chatID int64) (string, error) {
user, day, err := h.getUserToday(chatID)
if err != nil {
return "", err
}
last, err := h.DB.GetLastEvent(user.ID)
if err != nil {
return "", err
}
if last == nil {
return "", errors.New("no clock-in found — start with /clockin first")
}
if last.EventType == "out" {
return "", errors.New("already clocked out")
}
loc := loadLocation(user.Timezone)
now := time.Now().In(loc)
if err := h.DB.CreateEvent(user.ID, day.ID, "out", nil, now.Unix(), ""); err != nil {
return "", err
}
text := fmt.Sprintf("clocked out at %s", now.Format(TimeLayout))
// Award XP and update streaks if RPG enabled
st, _ := h.DB.GetOrCreateSettings(user.ID)
if st.RPGEnabled {
sessionWork := now.Unix() - last.OccurredAt
today := now.Format(DateLayout)
isWeekend := now.Weekday() == time.Saturday || now.Weekday() == time.Sunday
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)
text += fmt.Sprintf("\n\n+%d XP (Lv.%d)", xp, newLevel)
if leveledUp {
text += "\nLevel up!"
}
for _, a := range unlocked {
text += fmt.Sprintf("\nAchievement: %s", a)
}
}
return text, nil
}
// dayOff toggles today as a day off.
func (h *Handler) dayOff(chatID int64) (string, error) {
user, day, err := h.getUserToday(chatID)
if err != nil {
return "", err
}
if day.IsDayOff {
if err := h.DB.RemoveDayOff(user.ID, day.Date); err != nil {
return "", err
}
return "day off removed", nil
}
if err := h.DB.SetDayOff(user.ID, day.Date, ""); err != nil {
return "", err
}
return "today marked as day off", nil
}
// report returns today's work summary via buildDailyReport.
func (h *Handler) report(chatID int64) (string, error) {
user, day, err := h.getUserToday(chatID)
if err != nil {
return "", err
}
events, err := h.DB.EventsForDayByDayID(day.ID)
if err != nil {
return "", err
}
return h.buildDailyReport(user, day, events), nil
}
// toggleReport enables or disables the automatic daily report.
func (h *Handler) toggleReport(chatID int64) (string, error) {
user, err := h.getOrCreateUser(chatID)
if err != nil {
return "", err
}
st, err := h.DB.GetOrCreateSettings(user.ID)
if err != nil {
return "", err
}
st.ReportEnabled = !st.ReportEnabled
if err := h.DB.UpdateSettings(st); err != nil {
return "", err
}
if st.ReportEnabled {
return "daily report enabled", nil
}
return "daily report disabled", nil
}