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} } func (h *Handler) HandleMessage(update tgbotapi.Update) { msg := update.Message switch msg.Text { case "/start": h.handleStart(msg) case "/clockin", "Clock In", "ورود": h.handleClockInMsg(msg) case "/clockout", "Clock Out", "خروج": h.handleClockOutMsg(msg) 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") } } func (h *Handler) HandleCallback(update tgbotapi.Update) { cb := update.CallbackQuery chatID := cb.Message.Chat.ID msgID := cb.Message.MessageID switch cb.Data { case "clockin": h.clockInCallback(chatID, msgID, cb.ID) case "clockout": h.clockOutCallback(chatID, msgID, cb.ID) case "remote": h.toggleRemoteCallback(chatID, msgID, cb.ID) case "report": h.reportCallback(chatID, msgID, cb.ID) case "export": h.exportCallback(chatID, msgID, cb.ID) case "dayoff": h.dayOffCallback(chatID, msgID, cb.ID) case "back_menu": h.backToMenu(chatID, msgID, cb.ID) } } // --- /start with inline keyboard --- func (h *Handler) handleStart(msg *tgbotapi.Message) { text := "Welcome! Choose an action:" reply := tgbotapi.NewMessage(msg.Chat.ID, text) reply.ReplyMarkup = mainKeyboard() _, _ = h.Bot.Send(reply) kb := quickKeyboard() quick := tgbotapi.NewMessage(msg.Chat.ID, "Quick buttons added below:") quick.ReplyMarkup = kb _, _ = h.Bot.Send(quick) } func quickKeyboard() tgbotapi.ReplyKeyboardMarkup { return tgbotapi.NewReplyKeyboard( tgbotapi.NewKeyboardButtonRow( tgbotapi.NewKeyboardButton("Clock In"), tgbotapi.NewKeyboardButton("Clock Out"), ), ) } func mainKeyboard() tgbotapi.InlineKeyboardMarkup { return tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("Clock In", "clockin"), tgbotapi.NewInlineKeyboardButtonData("Clock Out", "clockout"), ), tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("Report", "report"), tgbotapi.NewInlineKeyboardButtonData("Export", "export"), ), tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("Toggle Remote/Onsite", "remote"), tgbotapi.NewInlineKeyboardButtonData("Day Off", "dayoff"), ), ) } func backKeyboard() tgbotapi.InlineKeyboardMarkup { return tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"), ), ) } // --- Message-only handlers (legacy text commands) --- func (h *Handler) handleClockInMsg(msg *tgbotapi.Message) { if err := h.doClockIn(); err != nil { h.replyWithError(msg.Chat.ID, err.Error()) } else { h.replyWithText(msg.Chat.ID, "clock_in") } } func (h *Handler) handleClockOutMsg(msg *tgbotapi.Message) { if err := h.doClockOut(); err != nil { h.replyWithError(msg.Chat.ID, err.Error()) } else { h.replyWithText(msg.Chat.ID, "clock_out") } } 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) } 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) } 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") } } 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") } } // --- Inline callback handlers --- func (h *Handler) clockInCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) text := h.Translator.Get("clock_in", "en") if err := h.doClockIn(); err != nil { text = err.Error() } edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) } func (h *Handler) clockOutCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) text := h.Translator.Get("clock_out", "en") if err := h.doClockOut(); err != nil { text = err.Error() } edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) } func (h *Handler) toggleRemoteCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) 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 { return } key := "remote_switched_on" if newMode == "onsite" { key = "remote_switched_off" } edit := tgbotapi.NewEditMessageText(chatID, msgID, h.Translator.Get(key, "en")) kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) } func (h *Handler) reportCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) today := time.Now().Format("2006-01-02") events, err := h.DB.EventsForDay(today) if err != nil { return } totals := ComputeDailyTotals(events) text := "No events today" if totals.PairCount > 0 { workStr := formatDuration(totals.TotalSeconds) breakStr := formatDuration(totals.BreakSeconds) text = fmt.Sprintf("Work: %s | Break: %s", workStr, breakStr) } edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) } func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) now := time.Now() year, month := now.Year(), now.Month() data, err := GenerateMonthlyReport(h.DB, year, month) if err != nil { edit := tgbotapi.NewEditMessageText(chatID, msgID, "Error generating report") kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) return } edit := tgbotapi.NewEditMessageText(chatID, msgID, "Here is your report:") kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{ Name: fmt.Sprintf("worktime_%s.xlsx", now.Format("2006_01")), Bytes: data, }) h.Bot.Send(doc) } func (h *Handler) dayOffCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) today := time.Now().Format("2006-01-02") isOff, err := h.DB.IsDayOff(today) if err != nil { return } text := h.Translator.Get("dayoff_set", "en") if isOff { h.DB.RemoveDayOff(today) text = h.Translator.Get("dayoff_removed", "en") } else { h.DB.SetDayOff(today, "") } edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) } func (h *Handler) backToMenu(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) edit := tgbotapi.NewEditMessageText(chatID, msgID, "Welcome! Choose an action:") kb := mainKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) } // --- Pure logic (no chat I/O) --- func (h *Handler) doClockIn() 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")) } if _, err := h.DB.GetSetting("remote_flag"); err != nil { _ = h.DB.SetSetting("remote_flag", "onsite") } return h.DB.CreateEvent("in", time.Now().Unix(), "") } func (h *Handler) doClockOut() 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")) } } // --- Helpers --- 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) } 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) }