Files
worktimeBot/internal/bot/handlers.go
db123 0a9175add4
All checks were successful
CI / build (push) Successful in 55s
handlers: replace fmt.Errorf with errors.New to satisfy vet
2026-06-23 20:50:03 +03:30

192 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bot
import (
"database/sql"
"errors"
"fmt"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"worktimeBot/internal/db"
"worktimeBot/pkg/i18n"
)
type Handler struct {
Bot *tgbotapi.BotAPI
DB *db.Store
Translator *i18n.Translator
}
func NewHandler(bot *tgbotapi.BotAPI, store *db.Store, tr *i18n.Translator) *Handler {
return &Handler{Bot: bot, DB: store, Translator: tr}
}
// HandleMessage routes incoming messages.
func (h *Handler) HandleMessage(update tgbotapi.Update) {
msg := update.Message
switch msg.Text {
case "/clockin":
if err := h.handleClockIn(); err != nil {
h.replyWithError(msg.Chat.ID, err.Error())
} else {
h.replyWithText(msg.Chat.ID, "clock_in")
}
case "/clockout":
if err := h.handleClockOut(); err != nil {
h.replyWithError(msg.Chat.ID, err.Error())
} else {
h.replyWithText(msg.Chat.ID, "clock_out")
}
case "/remote":
h.toggleRemote(msg)
case "/report":
h.handleReport(msg)
case "/export":
h.handleExport(msg)
case "/dayoff":
h.handleDayOff(msg)
default:
h.replyWithText(msg.Chat.ID, "unknown_command")
}
}
// handleClockIn validates and records a clockin event.
func (h *Handler) handleClockIn() error {
last, err := h.DB.GetLastEvent()
if err != nil && err != sql.ErrNoRows {
return err
}
if last != nil && last.EventType == "in" {
return errors.New(h.Translator.Get("already_clocked_in", "en"))
}
// Set default remote flag after first clockin.
if _, err := h.DB.GetSetting("remote_flag"); err != nil {
_ = h.DB.SetSetting("remote_flag", "onsite")
}
return h.DB.CreateEvent("in", time.Now().Unix(), "")
}
// handleClockOut validates and records a clockout event.
func (h *Handler) handleClockOut() error {
last, err := h.DB.GetLastEvent()
if err != nil {
return err
}
if last == nil {
return errors.New(h.Translator.Get("error", "en"))
}
switch last.EventType {
case "in":
return h.DB.CreateEvent("out", time.Now().Unix(), "")
case "out":
return errors.New(h.Translator.Get("already_clocked_out", "en"))
default:
return errors.New(h.Translator.Get("error", "en"))
}
}
// toggleRemote switches the remote/onsite flag.
func (h *Handler) toggleRemote(msg *tgbotapi.Message) {
current, err := h.DB.GetSetting("remote_flag")
if err != nil {
current = "onsite"
}
newMode := "remote"
if current == "remote" {
newMode = "onsite"
}
if err := h.DB.SetSetting("remote_flag", newMode); err != nil {
h.replyWithError(msg.Chat.ID, "error")
return
}
key := "remote_switched_on"
if newMode == "onsite" {
key = "remote_switched_off"
}
h.replyWithText(msg.Chat.ID, key)
}
// handleReport sends daily work time summary.
func (h *Handler) handleReport(msg *tgbotapi.Message) {
today := time.Now().Format("2006-01-02")
events, err := h.DB.EventsForDay(today)
if err != nil {
h.replyWithError(msg.Chat.ID, "error")
return
}
totals := ComputeDailyTotals(events)
if totals.PairCount == 0 {
h.replyWithText(msg.Chat.ID, "total_work_time")
return
}
workStr := formatDuration(totals.TotalSeconds)
breakStr := formatDuration(totals.BreakSeconds)
text := fmt.Sprintf("Work: %s | Break: %s", workStr, breakStr)
reply := tgbotapi.NewMessage(msg.Chat.ID, text)
_, _ = h.Bot.Send(reply)
}
// formatDuration converts seconds to a humanreadable string.
func formatDuration(seconds int64) string {
hours := seconds / 3600
mins := (seconds % 3600) / 60
if hours > 0 {
return fmt.Sprintf("%dh %dm", hours, mins)
}
return fmt.Sprintf("%dm", mins)
}
// handleExport generates and sends a monthly Excel report.
func (h *Handler) handleExport(msg *tgbotapi.Message) {
now := time.Now()
year, month := now.Year(), now.Month()
data, err := GenerateMonthlyReport(h.DB, year, month)
if err != nil {
h.replyWithError(msg.Chat.ID, "error")
return
}
doc := tgbotapi.NewDocument(msg.Chat.ID, tgbotapi.FileBytes{
Name: fmt.Sprintf("worktime_%s.xlsx", now.Format("2006_01")),
Bytes: data,
})
if _, err := h.Bot.Send(doc); err != nil {
h.replyWithError(msg.Chat.ID, "error")
}
}
// handleDayOff toggles today as a day off.
func (h *Handler) handleDayOff(msg *tgbotapi.Message) {
today := time.Now().Format("2006-01-02")
isOff, err := h.DB.IsDayOff(today)
if err != nil {
h.replyWithError(msg.Chat.ID, "error")
return
}
if isOff {
if err := h.DB.RemoveDayOff(today); err != nil {
h.replyWithError(msg.Chat.ID, "error")
return
}
h.replyWithText(msg.Chat.ID, "dayoff_removed")
} else {
if err := h.DB.SetDayOff(today, ""); err != nil {
h.replyWithError(msg.Chat.ID, "error")
return
}
h.replyWithText(msg.Chat.ID, "dayoff_set")
}
}
func (h *Handler) replyWithText(chatID int64, key string) {
text := h.Translator.Get(key, "en")
reply := tgbotapi.NewMessage(chatID, text)
_, _ = h.Bot.Send(reply)
}
func (h *Handler) replyWithError(chatID int64, key string) {
text := h.Translator.Get(key, "en")
reply := tgbotapi.NewMessage(chatID, text)
_, _ = h.Bot.Send(reply)
}