- Restore TotalWorkSeconds, CurrentStreak, LongestStreak writes in computeAndAwardXP and updateStreak (incremental on clock out) - Add recalcDayWorkSeconds and recalcAggregates for event edit path (recompute day + total + streak from scratch when events change) - Call recalcAggregates after editEventTimeSet, deleteEventConfirm, addEventDo in calendar.go - Remove runtime computeStreaks and totalWorkSeconds (N+1 scan) from RPG display, burnout, and clockOut - Add SetDailyWorkSeconds, SumDailyWorkSeconds store methods - Clean up unused dead functions and dead code in rpg.go
141 lines
3.8 KiB
Go
141 lines
3.8 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
|
|
|
|
stats, _ := h.DB.GetOrCreateRPGStats(user.ID)
|
|
h.updateStreak(stats, user.ID, today)
|
|
xp, leveledUp, newLevel, _ := h.computeAndAwardXP(stats, user.ID, sessionWork, stats.CurrentStreak, today)
|
|
totalHours := float64(stats.TotalWorkSeconds) / secondsPerHour
|
|
unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend, totalHours)
|
|
|
|
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
|
|
}
|