package handler import ( "context" "database/sql" "errors" "fmt" "time" "github.com/go-telegram/bot/models" "worktimeBot/internal/domain" ) // -- Commands -- func (h *Handler) handleClockIn(ctx context.Context, msg *models.Message, user *domain.User) { text, err := h.doClockIn(user) if err != nil { h.sendText(ctx, msg.Chat.ID, err.Error()) return } h.sendWithKB(ctx, msg.Chat.ID, text, mainKeyboard()) } func (h *Handler) handleClockOut(ctx context.Context, msg *models.Message, user *domain.User) { text, err := h.doClockOut(user) if err != nil { h.sendText(ctx, msg.Chat.ID, err.Error()) return } h.sendWithKB(ctx, msg.Chat.ID, text, mainKeyboard()) } func (h *Handler) handleDayOff(ctx context.Context, msg *models.Message, user *domain.User) { text, err := h.doDayOff(user) if err != nil { h.sendText(ctx, msg.Chat.ID, err.Error()) return } h.sendWithKB(ctx, msg.Chat.ID, text, mainKeyboard()) } func (h *Handler) handleReport(ctx context.Context, msg *models.Message, user *domain.User) { text, err := h.doReport(user) if err != nil { h.sendText(ctx, msg.Chat.ID, err.Error()) return } h.sendWithKB(ctx, msg.Chat.ID, text, mainKeyboard()) } func (h *Handler) handleReportToggle(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 } st.ReportEnabled = !st.ReportEnabled if err := h.Repo.UpdateSettings(st); err != nil { h.sendText(ctx, msg.Chat.ID, "Error saving settings") return } status := "disabled" if st.ReportEnabled { status = "enabled" } h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Daily report %s", status)) } // -- Callbacks -- func (h *Handler) clockInCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) { defer h.answerCb(ctx, cbID) text, err := h.doClockIn(user) if err != nil { text = err.Error() } kb := backKeyboard() h.editText(ctx, chatID, msgID, text, &kb) } func (h *Handler) clockOutCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) { defer h.answerCb(ctx, cbID) text, err := h.doClockOut(user) if err != nil { text = err.Error() } kb := backKeyboard() h.editText(ctx, chatID, msgID, text, &kb) } func (h *Handler) dayOffCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) { defer h.answerCb(ctx, cbID) text, err := h.doDayOff(user) if err != nil { text = err.Error() } kb := backKeyboard() h.editText(ctx, chatID, msgID, text, &kb) } func (h *Handler) reportCallback(ctx context.Context, chatID int64, msgID int, cbID string, user *domain.User) { defer h.answerCb(ctx, cbID) text, err := h.doReport(user) if err != nil { text = err.Error() } kb := backKeyboard() h.editText(ctx, chatID, msgID, text, &kb) } // -- Business logic (shared between commands and callbacks) -- func (h *Handler) doClockIn(user *domain.User) (string, error) { day, err := h.getTodayDay(user) if err != nil { return "", err } last, err := h.Repo.GetLastEvent(user.ID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return "", err } if last != nil && last.EventType == "in" { loc := h.loadLocation(user.Timezone) now := time.Now().In(loc) lastTime := time.Unix(last.OccurredAt, 0).In(loc) if lastTime.Format(domain.DateLayout) == now.Format(domain.DateLayout) { return "", errors.New("already clocked in") } } loc := h.loadLocation(user.Timezone) now := time.Now().In(loc) wt := day.CurrentWorkTypeID if err := h.Repo.CreateEvent(user.ID, day.ID, "in", &wt, now.Unix(), ""); err != nil { return "", err } return fmt.Sprintf("Clocked in at %s", now.Format(domain.TimeLayout)), nil } func (h *Handler) doClockOut(user *domain.User) (string, error) { day, err := h.getTodayDay(user) if err != nil { return "", err } last, err := h.Repo.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 := h.loadLocation(user.Timezone) now := time.Now().In(loc) if err := h.Repo.CreateEvent(user.ID, day.ID, "out", nil, now.Unix(), ""); err != nil { return "", err } text := fmt.Sprintf("Clocked out at %s", now.Format(domain.TimeLayout)) st, err := h.Repo.GetOrCreateSettings(user.ID) if err == nil && st.RPGEnabled { sessionWork := now.Unix() - last.OccurredAt today := now.Format(domain.DateLayout) isWeekend := now.Weekday() == time.Saturday || now.Weekday() == time.Sunday stats, err := h.Repo.GetOrCreateRPGStats(user.ID) if err == nil { text += h.awardXPAndCheckAchievements(user, stats, sessionWork, today, isWeekend) } } return text, nil } func (h *Handler) doDayOff(user *domain.User) (string, error) { loc := h.loadLocation(user.Timezone) today := time.Now().In(loc).Format(domain.DateLayout) day, err := h.Repo.GetOrCreateDay(user.ID, today) if err != nil { return "", err } if day.IsDayOff { if err := h.Repo.RemoveDayOff(user.ID, day.Date); err != nil { return "", err } return "Day off removed", nil } if err := h.Repo.SetDayOff(user.ID, day.Date, ""); err != nil { return "", err } return "Today marked as day off", nil } func (h *Handler) doReport(user *domain.User) (string, error) { day, err := h.getTodayDay(user) if err != nil { return "", err } events, err := h.Repo.EventsForDayByDayID(day.ID) if err != nil { return "", err } return h.buildDailyReport(user, day, events), nil } func (h *Handler) getTodayDay(user *domain.User) (*domain.Day, error) { loc := h.loadLocation(user.Timezone) today := time.Now().In(loc).Format(domain.DateLayout) return h.Repo.GetOrCreateDay(user.ID, today) } func (h *Handler) getTodayBreakThreshold(user *domain.User) int64 { loc := h.loadLocation(user.Timezone) today := time.Now().In(loc).Format(domain.DateLayout) day, err := h.Repo.GetDay(user.ID, today) if err != nil { return domain.DefaultBreakThreshold } return day.MinBreakThreshold }