package bot import ( "context" "fmt" "strconv" "strings" "time" "github.com/go-telegram/bot/models" "worktimeBot/internal/db" ) // handleSetReportTime parses a HH:MM argument and updates the user's report time. func (h *Handler) handleSetReportTime(ctx context.Context, msg *models.Message) { args := "" if len(msg.Text) > 16 { args = strings.TrimSpace(msg.Text[15:]) } if args == "" { user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } st, _ := h.DB.GetOrCreateSettings(user.ID) h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current report time: %s\nUsage: /setreporttime HH:MM (24-hour)", st.ReportTime)) return } if len(args) != 5 || args[2] != ':' { h.sendText(ctx, msg.Chat.ID, "Invalid format. Usage: /setreporttime HH:MM (24-hour)") return } hours, err1 := strconv.Atoi(args[:2]) mins, err2 := strconv.Atoi(args[3:]) if err1 != nil || err2 != nil || hours < 0 || hours > 23 || mins < 0 || mins > 59 { h.sendText(ctx, msg.Chat.ID, "Invalid time. Usage: /setreporttime HH:MM (24-hour)") return } user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error updating report time") return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error updating report time") return } st.ReportTime = args if err := h.DB.UpdateSettings(st); err != nil { h.sendText(ctx, msg.Chat.ID, "Error updating report time") return } h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Report time set to %s", args)) } // handleAccentMsg shows the accent color picker. func (h *Handler) handleAccentMsg(ctx context.Context, msg *models.Message) { user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } text, kb := h.buildAccentKeyboard(st) h.sendWithKB(ctx, msg.Chat.ID, text, kb) } // handleSetTimezone validates and updates the user's IANA timezone. func (h *Handler) handleSetTimezone(ctx context.Context, msg *models.Message) { args := "" if len(msg.Text) > 13 { // "/settimezone " is 12 chars args = strings.TrimSpace(msg.Text[13:]) } if args == "" { user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current timezone: %s\nUsage: /settimezone \nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone)) return } loc, err := time.LoadLocation(args) if err != nil { h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Invalid timezone: %s\nUse IANA format, e.g. Asia/Tehran, Europe/London", args)) return } user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } user.Timezone = args if err := h.DB.UpdateUser(user); err != nil { h.sendText(ctx, msg.Chat.ID, "Error saving timezone") return } now := time.Now().In(loc) h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Timezone set to %s (current time: %s)", args, now.Format(TimeLayout))) } // getTodayBreakThreshold returns today's break threshold in seconds for the given chat. func (h *Handler) getTodayBreakThreshold(chatID int64) int64 { user, err := h.getOrCreateUser(chatID) if err != nil { return defaultBreakThreshold } loc, err := time.LoadLocation(user.Timezone) if err != nil { loc = time.UTC } date := time.Now().In(loc).Format(DateLayout) day, err := h.DB.GetDay(user.ID, date) if err != nil { return defaultBreakThreshold } return day.MinBreakThreshold } // handleSetBreakMsg sets the minimum break threshold for today. func (h *Handler) handleSetBreakMsg(ctx context.Context, msg *models.Message) { args := strings.TrimSpace(strings.TrimPrefix(msg.Text, "/setbreak")) if args == "" { _, day, err := h.getUserToday(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current break threshold: %dm\nUsage: /setbreak (e.g. /setbreak 15)", day.MinBreakThreshold/60)) return } mins, err := strconv.Atoi(args) if err != nil || mins < 0 || mins > 480 { h.sendText(ctx, msg.Chat.ID, "Invalid value. Must be between 0 and 480 minutes (8 hours).") return } user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } now := time.Now() date := now.Format(DateLayout) day, err := h.DB.GetOrCreateDay(user.ID, date) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error updating break threshold") return } day.MinBreakThreshold = int64(mins) * 60 if err := h.DB.UpdateDay(day); err != nil { h.sendText(ctx, msg.Chat.ID, "Error saving break threshold") return } h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Break threshold set to %d minutes", mins)) } // settingsCallback shows the settings inline menu. func (h *Handler) settingsCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } bt := h.getTodayBreakThreshold(chatID) text, kb := h.buildSettingsKeyboard(user, st, bt) h.editText(ctx, chatID, msgID, text, &kb) } // buildSettingsKeyboard returns the settings menu text and inline keyboard. func (h *Handler) buildSettingsKeyboard(user *db.User, st *db.UserSettings, breakThreshold int64) (string, models.InlineKeyboardMarkup) { reportStatus := "off" if st.ReportEnabled { reportStatus = "on" } rpgStatus := "off" if st.RPGEnabled { rpgStatus = "on" } leagueStatus := "off" if st.LeagueOptIn { leagueStatus = "on" } rows := [][]models.InlineKeyboardButton{ { {Text: fmt.Sprintf("Timezone: %s", user.Timezone), CallbackData: "timezone"}, {Text: fmt.Sprintf("Calendar: %s", user.Calendar), CallbackData: "caltype"}, }, { {Text: fmt.Sprintf("Report: %s", reportStatus), CallbackData: "reporttoggle"}, {Text: fmt.Sprintf("Report time: %s", st.ReportTime), CallbackData: "reporttime"}, }, { {Text: fmt.Sprintf("Break: %dm", breakThreshold/60), CallbackData: "breakthreshold"}, {Text: fmt.Sprintf("Accent: %s", st.ExportAccent), CallbackData: "accent"}, }, { {Text: fmt.Sprintf("RPG: %s", rpgStatus), CallbackData: "rpgtoggle"}, {Text: fmt.Sprintf("League: %s", leagueStatus), CallbackData: "leaguetoggle"}, }, { {Text: fmt.Sprintf("Username: %s", user.Username), CallbackData: "username"}, {Text: fmt.Sprintf("Currency: %s", st.Currency), CallbackData: "currency"}, }, { {Text: fmt.Sprintf("Rate: %.2f", st.HourlyRate), CallbackData: "setrate"}, }, {{Text: "Back to Menu", CallbackData: "back_menu"}}, } return "Settings:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // usernameCallback shows the current username and how to change it. func (h *Handler) usernameCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text := fmt.Sprintf("Username: %s\n\nUse /setusername to change it.", user.Username) kb := models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{ {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } h.editText(ctx, chatID, msgID, text, &kb) } // accentCallback shows the accent color picker (inline edit). func (h *Handler) accentCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } text, kb := h.buildAccentKeyboard(st) h.editText(ctx, chatID, msgID, text, &kb) } // timezoneCallback shows the current timezone and instructions to change it. func (h *Handler) timezoneCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text := fmt.Sprintf("Current timezone: %s\n\nUse /settimezone to change it.\nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone) kb := models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{ {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } h.editText(ctx, chatID, msgID, text, &kb) } // buildAccentKeyboard returns the accent color picker keyboard. func (h *Handler) buildAccentKeyboard(st *db.UserSettings) (string, models.InlineKeyboardMarkup) { type accentOption struct { name string color string } accents := []accentOption{ {"ocean", "Blue"}, {"beach", "Warm"}, {"rose", "Pink"}, {"catppuccin", "Purple"}, } rows := [][]models.InlineKeyboardButton{} for _, a := range accents { label := fmt.Sprintf("%s (%s)", a.name, a.color) if a.name == st.ExportAccent { label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ {Text: label, CallbackData: "accent_" + a.name}, }) } rows = append(rows, []models.InlineKeyboardButton{ {Text: "Back to Settings", CallbackData: "back_settings"}, }) return "Select accent color:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // selectAccent sets the user's export accent color after validation. func (h *Handler) selectAccent(ctx context.Context, chatID int64, msgID int, callbackID, name string) { defer h.answerCb(ctx, 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 } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } st.ExportAccent = name if err := h.DB.UpdateSettings(st); err != nil { return } kb := models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{ {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } h.editText(ctx, chatID, msgID, fmt.Sprintf("Accent set to: %s", name), &kb) } // reportTimeCallback shows the current report time setting. func (h *Handler) reportTimeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } text := fmt.Sprintf("Current report time: %s\nUse /setreporttime HH:MM to change it.", st.ReportTime) kb := models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{ {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } h.editText(ctx, chatID, msgID, text, &kb) } // breakThresholdCallback shows the break threshold picker. func (h *Handler) breakThresholdCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) bt := h.getTodayBreakThreshold(chatID) text, kb := h.buildBreakThresholdKeyboard(bt) h.editText(ctx, chatID, msgID, text, &kb) } // buildBreakThresholdKeyboard returns the break threshold picker. func (h *Handler) buildBreakThresholdKeyboard(current int64) (string, models.InlineKeyboardMarkup) { values := []int{0, 5, 10, 15, 30, 60} labels := map[int]string{ 0: "0m", 5: "5m", 10: "10m", 15: "15m", 30: "30m", 60: "1h", } rows := [][]models.InlineKeyboardButton{} for _, v := range values { label := labels[v] if int(current/60) == v { label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ {Text: label, CallbackData: fmt.Sprintf("breakthreshold_%d", v)}, }) } rows = append(rows, []models.InlineKeyboardButton{ {Text: "Back to Settings", CallbackData: "back_settings"}, }) return "Select break threshold:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // selectBreakThreshold saves the chosen break threshold and returns to settings. func (h *Handler) selectBreakThreshold(ctx context.Context, chatID int64, msgID int, callbackID, valStr string) { defer h.answerCb(ctx, callbackID) mins, err := strconv.Atoi(valStr) if err != nil || mins < 0 || mins > maxBreakThresholdMins { return } user, err := h.getOrCreateUser(chatID) if err != nil { return } now := time.Now() date := now.Format(DateLayout) day, err := h.DB.GetOrCreateDay(user.ID, date) if err != nil { return } day.MinBreakThreshold = int64(mins) * 60 if err := h.DB.UpdateDay(day); err != nil { return } bt := h.getTodayBreakThreshold(chatID) st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } text, kb := h.buildSettingsKeyboard(user, st, bt) h.editText(ctx, chatID, msgID, text, &kb) } // backToSettings returns to the main settings menu. func (h *Handler) backToSettings(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } bt := h.getTodayBreakThreshold(chatID) text, kb := h.buildSettingsKeyboard(user, st, bt) h.editText(ctx, chatID, msgID, text, &kb) } // calTypeCallback shows the calendar type selector. func (h *Handler) calTypeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildCalendarKeyboard(user) h.editText(ctx, chatID, msgID, text, &kb) } // buildCalendarKeyboard returns the calendar type selector keyboard. func (h *Handler) buildCalendarKeyboard(user *db.User) (string, models.InlineKeyboardMarkup) { cals := []string{"gregorian", "jalali", "hijri"} calLabels := map[string]string{ "gregorian": "Gregorian", "jalali": "Jalali", "hijri": "Hijri", } rows := [][]models.InlineKeyboardButton{} for _, c := range cals { label := calLabels[c] if c == user.Calendar { label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ {Text: label, CallbackData: "caltype_" + c}, }) } rows = append(rows, []models.InlineKeyboardButton{ {Text: "Back to Settings", CallbackData: "back_settings"}, }) return "Select calendar type:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // selectCalendar sets the user's calendar after validation. func (h *Handler) selectCalendar(ctx context.Context, chatID int64, msgID int, callbackID, name string) { defer h.answerCb(ctx, 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 } bt := h.getTodayBreakThreshold(chatID) st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } text, kb := h.buildSettingsKeyboard(user, st, bt) h.editText(ctx, chatID, msgID, text, &kb) } // handleRPGSettingsMsg shows the RPG enable/disable picker (command). func (h *Handler) handleRPGSettingsMsg(ctx context.Context, msg *models.Message) { user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } text, kb := h.buildRPGToggleKeyboard(user.ID) h.sendWithKB(ctx, msg.Chat.ID, text, kb) } // handleLeagueSettingsMsg shows the League enable/disable picker (command). func (h *Handler) handleLeagueSettingsMsg(ctx context.Context, msg *models.Message) { user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") return } text, kb := h.buildLeagueToggleKeyboard(user.ID) h.sendWithKB(ctx, msg.Chat.ID, text, kb) } // rpgToggleCallback shows the RPG enable/disable picker (inline). func (h *Handler) rpgToggleCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildRPGToggleKeyboard(user.ID) h.editText(ctx, chatID, msgID, text, &kb) } // leagueToggleCallback shows the League enable/disable picker (inline). func (h *Handler) leagueToggleCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildLeagueToggleKeyboard(user.ID) h.editText(ctx, chatID, msgID, text, &kb) } // reportToggleCallback shows the Report enable/disable picker (inline) — replaces simple toggle. func (h *Handler) reportToggleCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildReportToggleKeyboard(user.ID) h.editText(ctx, chatID, msgID, text, &kb) } // buildRPGToggleKeyboard returns the RPG enable/disable picker. func (h *Handler) buildRPGToggleKeyboard(userID int64) (string, models.InlineKeyboardMarkup) { st, err := h.DB.GetOrCreateSettings(userID) current := false if err == nil { current = st.RPGEnabled } rows := [][]models.InlineKeyboardButton{} for _, opt := range []struct { label string data string value bool }{ {"Enabled", "rpg_enable", true}, {"Disabled", "rpg_disable", false}, } { label := opt.label if opt.value == current { label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ {Text: label, CallbackData: opt.data}, }) } rows = append(rows, []models.InlineKeyboardButton{ {Text: "Back to Settings", CallbackData: "back_settings"}, }) return "RPG System:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // buildLeagueToggleKeyboard returns the League enable/disable picker. func (h *Handler) buildLeagueToggleKeyboard(userID int64) (string, models.InlineKeyboardMarkup) { st, err := h.DB.GetOrCreateSettings(userID) current := false if err == nil { current = st.LeagueOptIn } rows := [][]models.InlineKeyboardButton{} for _, opt := range []struct { label string data string value bool }{ {"Enabled", "league_enable", true}, {"Disabled", "league_disable", false}, } { label := opt.label if opt.value == current { label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ {Text: label, CallbackData: opt.data}, }) } rows = append(rows, []models.InlineKeyboardButton{ {Text: "Back to Settings", CallbackData: "back_settings"}, }) return "WorkTime League:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // buildReportToggleKeyboard returns the Report enable/disable picker. func (h *Handler) buildReportToggleKeyboard(userID int64) (string, models.InlineKeyboardMarkup) { st, err := h.DB.GetOrCreateSettings(userID) current := true if err == nil { current = st.ReportEnabled } rows := [][]models.InlineKeyboardButton{} for _, opt := range []struct { label string data string value bool }{ {"Enabled", "report_enable", true}, {"Disabled", "report_disable", false}, } { label := opt.label if opt.value == current { label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ {Text: label, CallbackData: opt.data}, }) } rows = append(rows, []models.InlineKeyboardButton{ {Text: "Back to Settings", CallbackData: "back_settings"}, }) return "Daily Report:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } // handleSelectCallback handles binary select callbacks (report_enable, rpg_enable, etc.). func (h *Handler) handleSelectCallback(ctx context.Context, chatID int64, msgID int, callbackID string, setting string, value bool) { defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } st, err := h.DB.GetOrCreateSettings(user.ID) if err != nil { return } switch setting { case "report_enable": st.ReportEnabled = value case "rpg_enable": st.RPGEnabled = value if value { h.DB.GetOrCreateRPGStats(user.ID) } case "league_enable": st.LeagueOptIn = value } if err := h.DB.UpdateSettings(st); err != nil { return } bt := h.getTodayBreakThreshold(chatID) text, kb := h.buildSettingsKeyboard(user, st, bt) h.editText(ctx, chatID, msgID, text, &kb) }