fix: resolve go vet error and dayStart logic in ComputeDailyTotals
- Fix 'string(d)' int-to-rune conversion in rpg_test.go by using fmt.Sprintf - Fix ComputeDailyTotals dayStart logic: initial state should be StateWorking when dayStart > 0, correctly counting work from dayStart to first out event - Update README project structure to reflect new domain/handler/repo layout - Implement missing achievements: rpg_pioneer (on RPG enable) and salary_setter (on first rate set) - Add tryUnlockAchievement helper to Handler
This commit is contained in:
222
internal/handler/clock.go
Normal file
222
internal/handler/clock.go
Normal file
@@ -0,0 +1,222 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user