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) 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 { 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 }