Files
worktimeBot/internal/bot/settings.go
db123 5f12e3838b feat: add break threshold picker UI to settings with preset values
Replaces the text-only instructions with a grid of common break thresholds (0/5/10/15/30 min, 1/2/4/8h) matching the accent picker pattern.
2026-06-24 20:59:49 +03:30

405 lines
13 KiB
Go

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 { // "/setreporttime " is 15 chars
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
}
h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current report time: %s\nUsage: /setreporttime HH:MM (24-hour)", user.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
}
user.ReportTime = args
if err := h.DB.UpdateUser(user); 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
}
text, kb := h.buildAccentKeyboard(user)
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 <IANA timezone>\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("15:04")))
}
// 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 300
}
now := time.Now()
date := now.Format("2006-01-02")
day, err := h.DB.GetDay(user.ID, date)
if err != nil {
return 300
}
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 <minutes> (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("2006-01-02")
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) {
h.answerCb(ctx, callbackID)
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
bt := h.getTodayBreakThreshold(chatID)
text, kb := h.buildSettingsKeyboard(user, bt)
h.editText(ctx, chatID, msgID, text, &kb)
}
// buildSettingsKeyboard returns the settings menu text and inline keyboard.
func (h *Handler) buildSettingsKeyboard(user *db.User, breakThreshold int64) (string, models.InlineKeyboardMarkup) {
reportStatus := "disabled"
if user.ReportEnabled {
reportStatus = "enabled"
}
rows := [][]models.InlineKeyboardButton{
{{Text: fmt.Sprintf("Timezone: %s", user.Timezone), CallbackData: "timezone"}},
{{Text: fmt.Sprintf("Accent: %s", user.ExportAccent), CallbackData: "accent"}},
{{Text: fmt.Sprintf("Calendar: %s", user.Calendar), CallbackData: "caltype"}},
{{Text: fmt.Sprintf("Report: %s", reportStatus), CallbackData: "reporttoggle"}},
{{Text: fmt.Sprintf("Report time: %s", user.ReportTime), CallbackData: "reporttime"}},
{{Text: fmt.Sprintf("Break threshold: %dm", breakThreshold/60), CallbackData: "breakthreshold"}},
{{Text: "History", CallbackData: "history"}},
{{Text: "Back to Menu", CallbackData: "back_menu"}},
}
return "Settings:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
}
// accentCallback shows the accent color picker (inline edit).
func (h *Handler) accentCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
h.answerCb(ctx, callbackID)
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text, kb := h.buildAccentKeyboard(user)
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) {
h.answerCb(ctx, 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 := 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(user *db.User) (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 == user.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) {
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
}
user.ExportAccent = name
if err := h.DB.UpdateUser(user); 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) {
h.answerCb(ctx, 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)
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) {
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, 120, 240, 480}
labels := map[int]string{
0: "0 (off)",
5: "5 min",
10: "10 min",
15: "15 min",
30: "30 min",
60: "1h",
120: "2h",
240: "4h",
480: "8h",
}
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) {
h.answerCb(ctx, callbackID)
mins, err := strconv.Atoi(valStr)
if err != nil || mins < 0 || mins > 480 {
return
}
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
now := time.Now()
date := now.Format("2006-01-02")
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)
text, kb := h.buildSettingsKeyboard(user, 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) {
h.answerCb(ctx, callbackID)
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
bt := h.getTodayBreakThreshold(chatID)
text, kb := h.buildSettingsKeyboard(user, 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) {
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) {
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)
text, kb := h.buildSettingsKeyboard(user, bt)
h.editText(ctx, chatID, msgID, text, &kb)
}