diff --git a/internal/bot/calendar.go b/internal/bot/calendar.go index ec6b585..68d7086 100644 --- a/internal/bot/calendar.go +++ b/internal/bot/calendar.go @@ -79,6 +79,37 @@ func (h *Handler) handleHistoryCallback(ctx context.Context, chatID int64, msgID h.editCalendar(ctx, chatID, msgID, y, m) return } + // Navigate to previous year + if n, _ := fmt.Sscanf(data, "cal_prev_year_%d_%d", &y, &m); n == 2 { + h.editCalendar(ctx, chatID, msgID, y, m) + return + } + // Navigate to next year + if n, _ := fmt.Sscanf(data, "cal_next_year_%d_%d", &y, &m); n == 2 { + h.editCalendar(ctx, chatID, msgID, y, m) + return + } + // Show year picker from calendar + if n, _ := fmt.Sscanf(data, "cal_show_years_%d_%d", &y, &m); n == 2 { + h.editCalendarYearPicker(ctx, chatID, msgID, y, m, y) + return + } + // Navigate year block in picker + var startY, refY, refM int + if n, _ := fmt.Sscanf(data, "cal_years_%d_%d_%d", &startY, &refY, &refM); n == 3 { + h.editCalendarYearPicker(ctx, chatID, msgID, startY+6, refM, refY) + return + } + // Select year from picker + if n, _ := fmt.Sscanf(data, "cal_year_%d", &y); n == 1 { + h.editCalendar(ctx, chatID, msgID, y, 1) + return + } + // Back from year picker to calendar + if n, _ := fmt.Sscanf(data, "cal_years_back_%d_%d", &y, &m); n == 2 { + h.editCalendar(ctx, chatID, msgID, y, m) + return + } // Open a specific day if n, _ := fmt.Sscanf(data, "cal_day_%s", &date); n == 1 && len(date) == 10 { h.editDayView(ctx, chatID, msgID, user, date, loc) @@ -148,6 +179,36 @@ func (h *Handler) sendCalendar(ctx context.Context, chatID int64, msgID int, yea h.editCalendar(ctx, chatID, msgID, year, month) } +// buildYearPicker returns a 12-year grid keyboard centered on centerYear. +// cbPrefix is "cal_" or "exp_". backData is the callback for the Back button. +// navSuffix is appended to the nav callbacks to carry reference info (e.g. "_2026_3"). +func buildYearPicker(cbPrefix, backData, navSuffix string, centerYear int) models.InlineKeyboardMarkup { + blockStart := centerYear - 6 + kbRows := [][]models.InlineKeyboardButton{ + { + {Text: "< 12", CallbackData: fmt.Sprintf("%syears_%d%s", cbPrefix, blockStart-12, navSuffix)}, + {Text: fmt.Sprintf("%d", centerYear), CallbackData: "noop"}, + {Text: "12 >", CallbackData: fmt.Sprintf("%syears_%d%s", cbPrefix, blockStart+12, navSuffix)}, + }, + } + for i := 0; i < 12; i += 4 { + row := []models.InlineKeyboardButton{} + for j := i; j < i+4; j++ { + y := blockStart + j + row = append(row, models.InlineKeyboardButton{ + Text: fmt.Sprintf("%d", y), CallbackData: fmt.Sprintf("%syear_%d", cbPrefix, y), + }) + } + kbRows = append(kbRows, row) + } + if backData != "" { + kbRows = append(kbRows, []models.InlineKeyboardButton{ + {Text: "Back", CallbackData: backData}, + }) + } + return models.InlineKeyboardMarkup{InlineKeyboard: kbRows} +} + // editCalendar renders a monthly calendar grid with event indicators and navigation. func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, year, month int) { user, err := h.getOrCreateUser(chatID) @@ -193,6 +254,13 @@ func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, yea kbRows := [][]models.InlineKeyboardButton{} + // Year navigation row + kbRows = append(kbRows, []models.InlineKeyboardButton{ + {Text: fmt.Sprintf("< %d", year-1), CallbackData: fmt.Sprintf("cal_prev_year_%d_%d", year-1, month)}, + {Text: fmt.Sprintf("%d", year), CallbackData: fmt.Sprintf("cal_show_years_%d_%d", year, month)}, + {Text: fmt.Sprintf("%d >", year+1), CallbackData: fmt.Sprintf("cal_next_year_%d_%d", year+1, month)}, + }) + // Weekday header row headerRow := []models.InlineKeyboardButton{} for _, wn := range cm.weekDays { @@ -241,6 +309,17 @@ func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, yea h.sendOrEdit(ctx, chatID, msgID, text, &kb) } +// editCalendarYearPicker shows a 12-year grid for year selection. +// refY/refM encode the view to return to via the Back button. +func (h *Handler) editCalendarYearPicker(ctx context.Context, chatID int64, msgID int, centerYear, refM, refY int) { + backData := fmt.Sprintf("cal_years_back_%d_%d", refY, refM) + navSuffix := fmt.Sprintf("_%d_%d", refY, refM) + kb := buildYearPicker("cal_", backData, navSuffix, centerYear) + h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chatID, MessageID: msgID, Text: "Select year:", ReplyMarkup: &kb, + }) +} + // editDayView opens an existing message as a day view. func (h *Handler) editDayView(ctx context.Context, chatID int64, msgID int, user *db.User, date string, loc *time.Location) { h.sendDayView(ctx, chatID, msgID, user, date, loc, false) diff --git a/internal/bot/export.go b/internal/bot/export.go index 6e7652d..5525759 100644 --- a/internal/bot/export.go +++ b/internal/bot/export.go @@ -377,7 +377,7 @@ func (h *Handler) editExportMonthPicker(ctx context.Context, chatID int64, msgID // Year navigation { {Text: "<", CallbackData: fmt.Sprintf("exp_year_prev_%d", year)}, - {Text: fmt.Sprintf("%d", year), CallbackData: "noop"}, + {Text: fmt.Sprintf("%d", year), CallbackData: fmt.Sprintf("exp_show_years_%d", year)}, {Text: ">", CallbackData: fmt.Sprintf("exp_year_next_%d", year)}, }, } @@ -405,6 +405,17 @@ func (h *Handler) editExportMonthPicker(ctx context.Context, chatID int64, msgID } } +// editExportYearPicker shows a 12-year grid for year selection in the export flow. +// refY is the year to return to via the Back button. +func (h *Handler) editExportYearPicker(ctx context.Context, chatID int64, msgID int, centerYear, refY int) { + backData := fmt.Sprintf("exp_years_back_%d", refY) + navSuffix := fmt.Sprintf("_%d", refY) + kb := buildYearPicker("exp_", backData, navSuffix, centerYear) + h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chatID, MessageID: msgID, Text: "Select year:", ReplyMarkup: &kb, + }) +} + // formatMonthTitle returns the localized month name and year for a given calendar. func formatMonthTitle(cal string, year, month int) string { switch cal { @@ -437,6 +448,27 @@ func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID h.editExportMonthPicker(ctx, chatID, msgID, y+1, 0, user) return } + // Show year picker from month picker + if n, _ := fmt.Sscanf(data, "exp_show_years_%d", &y); n == 1 { + h.editExportYearPicker(ctx, chatID, msgID, y, y) + return + } + // Navigate year block in picker + var refY int + if n, _ := fmt.Sscanf(data, "exp_years_%d_%d", &y, &refY); n == 2 { + h.editExportYearPicker(ctx, chatID, msgID, y+6, refY) + return + } + // Select year from picker + if n, _ := fmt.Sscanf(data, "exp_year_%d", &y); n == 1 { + h.editExportMonthPicker(ctx, chatID, msgID, y, 0, user) + return + } + // Back from year picker to month picker + if n, _ := fmt.Sscanf(data, "exp_years_back_%d", &y); n == 1 { + h.editExportMonthPicker(ctx, chatID, msgID, y, 0, user) + return + } // Export a specific month if n, _ := fmt.Sscanf(data, "exp_do_%d_%d", &y, &m); n == 2 {