refactor: split handlers.go into 5 files, redesign export month picker, pad calendar rows, improve comments

- Split 1571-line handlers.go into: handlers.go (core), clock.go, settings.go, calendar.go, report.go
- Redesigned export month picker: year navigation + 12-month grid instead of prev/next month
- Fixed calendar last row: pad remaining cells with empty buttons to ensure 7 columns
- Added Go doc comments across all files (dateutil.go, totals.go, store.go, main.go, webhook.go)
This commit is contained in:
2026-06-24 11:33:17 +03:30
parent d8599657f4
commit a8b849f8fd
11 changed files with 1366 additions and 1176 deletions

308
internal/bot/settings.go Normal file
View File

@@ -0,0 +1,308 @@
package bot
import (
"fmt"
"strconv"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"worktimeBot/internal/db"
)
// handleSetReportTime parses a HH:MM argument and updates the user's report time.
func (h *Handler) handleSetReportTime(msg *tgbotapi.Message) {
arg := msg.CommandArguments()
if arg == "" {
user, err := h.getOrCreateUser(msg.Chat.ID)
if err != nil {
h.sendText(msg.Chat.ID, "Error loading profile")
return
}
h.sendText(msg.Chat.ID, fmt.Sprintf("Current report time: %s\nUsage: /setreporttime HH:MM (24-hour)", user.ReportTime))
return
}
if len(arg) != 5 || arg[2] != ':' {
h.sendText(msg.Chat.ID, "Invalid format. Usage: /setreporttime HH:MM (24-hour)")
return
}
hours, err1 := strconv.Atoi(arg[:2])
mins, err2 := strconv.Atoi(arg[3:])
if err1 != nil || err2 != nil || hours < 0 || hours > 23 || mins < 0 || mins > 59 {
h.sendText(msg.Chat.ID, "Invalid time. Usage: /setreporttime HH:MM (24-hour)")
return
}
user, err := h.getOrCreateUser(msg.Chat.ID)
if err != nil {
h.sendText(msg.Chat.ID, "Error updating report time")
return
}
user.ReportTime = arg
if err := h.DB.UpdateUser(user); err != nil {
h.sendText(msg.Chat.ID, "Error updating report time")
return
}
h.sendText(msg.Chat.ID, fmt.Sprintf("Report time set to %s", arg))
}
// handleAccentMsg shows the accent color picker.
func (h *Handler) handleAccentMsg(msg *tgbotapi.Message) {
user, err := h.getOrCreateUser(msg.Chat.ID)
if err != nil {
h.sendText(msg.Chat.ID, "Error loading profile")
return
}
text, kb := h.buildAccentKeyboard(user)
reply := tgbotapi.NewMessage(msg.Chat.ID, text)
reply.ReplyMarkup = &kb
h.Bot.Send(reply)
}
// handleSetTimezone validates and updates the user's IANA timezone.
func (h *Handler) handleSetTimezone(msg *tgbotapi.Message) {
arg := msg.CommandArguments()
if arg == "" {
user, err := h.getOrCreateUser(msg.Chat.ID)
if err != nil {
h.sendText(msg.Chat.ID, "Error loading profile")
return
}
h.sendText(msg.Chat.ID, fmt.Sprintf("Current timezone: %s\nUsage: /settimezone <IANA timezone>\nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone))
return
}
loc, err := time.LoadLocation(arg)
if err != nil {
h.sendText(msg.Chat.ID, fmt.Sprintf("Invalid timezone: %s\nUse IANA format, e.g. Asia/Tehran, Europe/London", arg))
return
}
user, err := h.getOrCreateUser(msg.Chat.ID)
if err != nil {
h.sendText(msg.Chat.ID, "Error loading profile")
return
}
user.Timezone = arg
if err := h.DB.UpdateUser(user); err != nil {
h.sendText(msg.Chat.ID, "Error saving timezone")
return
}
now := time.Now().In(loc)
h.sendText(msg.Chat.ID, fmt.Sprintf("Timezone set to %s (current time: %s)", arg, now.Format("15:04")))
}
// settingsCallback shows the settings inline menu.
func (h *Handler) settingsCallback(chatID int64, msgID int, callbackID string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text, kb := h.buildSettingsKeyboard(user)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// buildSettingsKeyboard returns the settings menu text and inline keyboard.
func (h *Handler) buildSettingsKeyboard(user *db.User) (string, tgbotapi.InlineKeyboardMarkup) {
reportStatus := "disabled"
if user.ReportEnabled {
reportStatus = "enabled"
}
rows := [][]tgbotapi.InlineKeyboardButton{
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Timezone: %s", user.Timezone), "timezone"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Accent: %s", user.ExportAccent), "accent"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Calendar: %s", user.Calendar), "caltype"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Report: %s", reportStatus), "reporttoggle"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Report time: %s", user.ReportTime), "reporttime"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("History", "history"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
),
}
return "Settings:", tgbotapi.NewInlineKeyboardMarkup(rows...)
}
// accentCallback shows the accent color picker (inline edit).
func (h *Handler) accentCallback(chatID int64, msgID int, callbackID string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text, kb := h.buildAccentKeyboard(user)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// timezoneCallback shows the current timezone and instructions to change it.
func (h *Handler) timezoneCallback(chatID int64, msgID int, callbackID string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text := fmt.Sprintf("Current timezone: %s\n\nUse /settimezone <name> to change it.\nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone)
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
),
)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// buildAccentKeyboard returns the accent color picker keyboard.
func (h *Handler) buildAccentKeyboard(user *db.User) (string, tgbotapi.InlineKeyboardMarkup) {
type accentOption struct {
name string
color string
}
accents := []accentOption{
{"ocean", "Blue"},
{"beach", "Warm"},
{"rose", "Pink"},
{"catppuccin", "Purple"},
}
rows := [][]tgbotapi.InlineKeyboardButton{}
for _, a := range accents {
label := fmt.Sprintf("%s (%s)", a.name, a.color)
if a.name == user.ExportAccent {
label = "> " + label
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(label, "accent_"+a.name),
))
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
))
return "Select accent color:", tgbotapi.NewInlineKeyboardMarkup(rows...)
}
// selectAccent sets the user's export accent color after validation.
func (h *Handler) selectAccent(chatID int64, msgID int, callbackID, name string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
valid := map[string]bool{"ocean": true, "beach": true, "rose": true, "catppuccin": true}
if !valid[name] {
return
}
user.ExportAccent = name
if err := h.DB.UpdateUser(user); err != nil {
return
}
edit := tgbotapi.NewEditMessageText(chatID, msgID, fmt.Sprintf("Accent set to: %s", name))
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
),
)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// reportTimeCallback shows the current report time setting.
func (h *Handler) reportTimeCallback(chatID int64, msgID int, callbackID string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text := fmt.Sprintf("Current report time: %s\nUse /setreporttime HH:MM to change it.", user.ReportTime)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
),
)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// backToSettings returns to the main settings menu.
func (h *Handler) backToSettings(chatID int64, msgID int, callbackID string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text, kb := h.buildSettingsKeyboard(user)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// calTypeCallback shows the calendar type selector.
func (h *Handler) calTypeCallback(chatID int64, msgID int, callbackID string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text, kb := h.buildCalendarKeyboard(user)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}
// buildCalendarKeyboard returns the calendar type selector keyboard.
func (h *Handler) buildCalendarKeyboard(user *db.User) (string, tgbotapi.InlineKeyboardMarkup) {
cals := []string{"gregorian", "jalali", "hijri"}
calLabels := map[string]string{
"gregorian": "Gregorian",
"jalali": "Jalali",
"hijri": "Hijri",
}
rows := [][]tgbotapi.InlineKeyboardButton{}
for _, c := range cals {
label := calLabels[c]
if c == user.Calendar {
label = "> " + label
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(label, "caltype_"+c),
))
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
))
return "Select calendar type:", tgbotapi.NewInlineKeyboardMarkup(rows...)
}
// selectCalendar sets the user's calendar after validation.
func (h *Handler) selectCalendar(chatID int64, msgID int, callbackID, name string) {
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
valid := map[string]bool{"gregorian": true, "jalali": true, "hijri": true}
if !valid[name] {
return
}
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
user.Calendar = name
if err := h.DB.UpdateUser(user); err != nil {
return
}
text, kb := h.buildSettingsKeyboard(user)
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ReplyMarkup = &kb
h.Bot.Send(edit)
}