diff --git a/telegram_bot/bot/bot.go b/telegram_bot/bot/bot.go index dab2da6..75c1309 100644 --- a/telegram_bot/bot/bot.go +++ b/telegram_bot/bot/bot.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "example.com/m/adapter/backend" "github.com/NicoNex/echotron/v3" @@ -37,6 +38,7 @@ type Exec struct { type Bot struct { echotron.API + mu sync.Mutex ChatID int64 exec *Exec CurrentState State @@ -205,6 +207,9 @@ func (b *Bot) SendMessageWithURLButton(text, buttonText, url string) { } func (b *Bot) Update(u *echotron.Update) { + b.mu.Lock() + defer b.mu.Unlock() + log.Info(). Int64("chat_id", b.ChatID). Str("current_state", fmt.Sprintf("%T", b.CurrentState)). diff --git a/telegram_bot/screens/accept_workspace_invite.go b/telegram_bot/screens/accept_workspace_invite.go index 81a9731..d2a35f4 100644 --- a/telegram_bot/screens/accept_workspace_invite.go +++ b/telegram_bot/screens/accept_workspace_invite.go @@ -12,6 +12,11 @@ type AcceptWorkspaceInvite struct { InviteID string } +const msgAcceptWorkspaceInviteSuccessfully = ` +✅ Вы присоединились к рабочему пространству! +Администратор сможет выдать вам необходимые права в веб-интерфейсе. +` + func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) { // Получаем JWT токен jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID) @@ -29,11 +34,7 @@ func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) { return } - // Успешно принято - text := "✅ Вы присоединились к рабочему пространству!\n\n" + - "Администратор сможет выдать вам необходимые права в веб-интерфейсе." - - b.SendNew(text, bot.Keyboard( + b.SendNew(msgAcceptWorkspaceInviteSuccessfully, bot.Keyboard( bot.Row(bot.Button("🏠 Главное меню", "menu")), )) } diff --git a/telegram_bot/screens/add_purchase.go b/telegram_bot/screens/add_purchase.go index 0af80a0..0354594 100644 --- a/telegram_bot/screens/add_purchase.go +++ b/telegram_bot/screens/add_purchase.go @@ -5,6 +5,7 @@ import ( "fmt" "example.com/m/bot" + "example.com/m/ui" "github.com/NicoNex/echotron/v3" "github.com/rs/zerolog/log" ) @@ -84,26 +85,31 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin if len(page.Items) == 0 { text += "У вас нет проектов. Создайте проект и попробуйте снова.\n\n" } else { - pageInfo := "" - if page.Pages > 1 { - pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages) - } + text += fmt.Sprintf("Проекты%s\n\n", ui.FormatPageInfo(s.ProjectPage, page.Pages)) - text += fmt.Sprintf("Проекты%s\n\n", pageInfo) + var slots []echotron.InlineKeyboardButton for _, project := range page.Items { - buttons = append(buttons, bot.Row( - bot.Button(project.Title, fmt.Sprintf("project:%s", project.ID)), - )) + slots = append(slots, bot.Button(project.Title, fmt.Sprintf("project:%s", project.ID))) + } + if page.Pages >= 2 { + for len(slots) < addPurchaseProjectsPerPage { + slots = append(slots, bot.Button(" ", "empty")) + } + } + for i := 0; i < len(slots); i += 2 { + row := []echotron.InlineKeyboardButton{slots[i]} + if i+1 < len(slots) { + row = append(row, slots[i+1]) + } else { + row = append(row, bot.Button(" ", "empty")) + } + buttons = append(buttons, row) } - var navRow []echotron.InlineKeyboardButton - if s.ProjectPage > 0 { - navRow = append(navRow, bot.Button("←", "prev_project")) - } - if s.ProjectPage+1 < page.Pages { - navRow = append(navRow, bot.Button("→", "next_project")) - } - if len(navRow) > 0 { + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.ProjectPage, + TotalPages: page.Pages, + }); navRow != nil { buttons = append(buttons, navRow) } } @@ -146,27 +152,31 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin Вернитесь назад и создайте креатив в разделе Креативы` } else { - pageInfo := "" - if page.Pages > 1 { - pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages) - } - - text += fmt.Sprintf("Креативы%s\n\n", pageInfo) + text += fmt.Sprintf("Креативы%s\n\n", ui.FormatPageInfo(s.CreativePage, page.Pages)) + var slots []echotron.InlineKeyboardButton for _, creative := range page.Items { - buttons = append(buttons, bot.Row( - bot.Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)), - )) + slots = append(slots, bot.Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID))) + } + if page.Pages >= 2 { + for len(slots) < addPurchaseCreativesPerPage { + slots = append(slots, bot.Button(" ", "empty")) + } + } + for i := 0; i < len(slots); i += 2 { + row := []echotron.InlineKeyboardButton{slots[i]} + if i+1 < len(slots) { + row = append(row, slots[i+1]) + } else { + row = append(row, bot.Button(" ", "empty")) + } + buttons = append(buttons, row) } - var navRow []echotron.InlineKeyboardButton - if s.CreativePage > 0 { - navRow = append(navRow, bot.Button("←", "prev_creative")) - } - if s.CreativePage+1 < page.Pages { - navRow = append(navRow, bot.Button("→", "next_creative")) - } - if len(navRow) > 0 { + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.CreativePage, + TotalPages: page.Pages, + }); navRow != nil { buttons = append(buttons, navRow) } } @@ -232,27 +242,33 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) { s.ActivePicker = "" s.Enter(b, bot.EditMessage) - case "prev_project": - if s.ProjectPage > 0 { - s.ProjectPage-- + case "prev": + if s.ActivePicker == "project_list" { + if s.ProjectPage > 0 { + s.ProjectPage-- + } + s.Enter(b, bot.EditMessage) + return } - s.Enter(b, bot.EditMessage) - - case "next_project": - s.ProjectPage++ - s.Enter(b, bot.EditMessage) - - case "prev_creative": - if s.CreativePage > 0 { - s.CreativePage-- + if s.ActivePicker == "creative_list" { + if s.CreativePage > 0 { + s.CreativePage-- + } + s.Enter(b, bot.EditMessage) + return } - s.Enter(b, bot.EditMessage) - - case "next_creative": - s.CreativePage++ - s.Enter(b, bot.EditMessage) case "next": + if s.ActivePicker == "project_list" { + s.ProjectPage++ + s.Enter(b, bot.EditMessage) + return + } + if s.ActivePicker == "creative_list" { + s.CreativePage++ + s.Enter(b, bot.EditMessage) + return + } if s.ProjectID != "" && s.CreativeID != "" { b.SetState(&SelectChannelsForPurchase{ WorkspaceID: s.WorkspaceID, diff --git a/telegram_bot/screens/creatives.go b/telegram_bot/screens/creatives.go index d2c5f00..fa8b009 100644 --- a/telegram_bot/screens/creatives.go +++ b/telegram_bot/screens/creatives.go @@ -6,6 +6,7 @@ import ( "strings" "example.com/m/bot" + "example.com/m/ui" "github.com/NicoNex/echotron/v3" "github.com/rs/zerolog/log" ) @@ -71,11 +72,6 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) Начните с добавления первого креатива`, navPath) } else { - pageInfo := "" - if page.Pages > 1 { - pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages) - } - text = fmt.Sprintf(`%s Креативы%s @@ -84,7 +80,7 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) Выберите креатив`, navPath, - pageInfo) + ui.FormatPageInfo(s.CurrentPage, page.Pages)) } // Создаем слоты для креативов @@ -117,23 +113,14 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) } // Ряд: Пагинация + Добавить (динамическая ширина) - var navRow []echotron.InlineKeyboardButton - - // Левая стрелка (только если есть предыдущая страница) - if s.CurrentPage > 0 { - navRow = append(navRow, bot.Button("←", "prev")) + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.CurrentPage, + TotalPages: page.Pages, + MiddleButtons: []echotron.InlineKeyboardButton{bot.Button("+ Добавить", "add_creative")}, + }); navRow != nil { + buttons = append(buttons, navRow) } - // Кнопка "Добавить креатив" всегда в центре - navRow = append(navRow, bot.Button("+ Добавить", "add_creative")) - - // Правая стрелка (только если есть следующая страница) - if s.CurrentPage+1 < page.Pages { - navRow = append(navRow, bot.Button("→", "next")) - } - - buttons = append(buttons, navRow) - // Нижний ряд: Назад + Архив buttons = append(buttons, bot.Row( bot.Button("← Назад", "back"), diff --git a/telegram_bot/screens/my_placements.go b/telegram_bot/screens/my_placements.go index 9d82801..03343fd 100644 --- a/telegram_bot/screens/my_placements.go +++ b/telegram_bot/screens/my_placements.go @@ -6,6 +6,7 @@ import ( "strings" "example.com/m/bot" + "example.com/m/ui" "github.com/NicoNex/echotron/v3" "github.com/rs/zerolog/log" ) @@ -75,62 +76,40 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str Размещения создаются автоматически при создании закупа` } else { - pageInfo := "" - if page.Pages > 1 { - pageInfo = fmt.Sprintf(" (стр. %d/%d)", s.CurrentPage+1, page.Pages) - } - text += fmt.Sprintf("Выберите размещение для деталей%s\n\n", pageInfo) + text += fmt.Sprintf("Выберите размещение для деталей%s\n\n", ui.FormatPageInfo(s.CurrentPage, page.Pages)) // Размещения по 2 в ряду - for i := 0; i < len(page.Items); i += 2 { - var row []echotron.InlineKeyboardButton - - // Первый placement в ряду - placement := page.Items[i] + var slots []echotron.InlineKeyboardButton + for _, placement := range page.Items { placementName := placement.CreativeName if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" { placementName = fmt.Sprintf("%s → %s", placement.CreativeName, *placement.PlacementChannelTitle) } - row = append(row, bot.Button(placementName, fmt.Sprintf("placement:%s", placement.ID))) - - // Второй placement в ряду (если есть) - if i+1 < len(page.Items) { - placement = page.Items[i+1] - placementName = placement.CreativeName - if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" { - placementName = fmt.Sprintf("%s → %s", placement.CreativeName, *placement.PlacementChannelTitle) - } - row = append(row, bot.Button(placementName, fmt.Sprintf("placement:%s", placement.ID))) + slots = append(slots, bot.Button(placementName, fmt.Sprintf("placement:%s", placement.ID))) + } + if page.Pages >= 2 { + for len(slots) < placementsPerPage { + slots = append(slots, bot.Button(" ", "empty")) + } + } + for i := 0; i < len(slots); i += 2 { + row := []echotron.InlineKeyboardButton{slots[i]} + if i+1 < len(slots) { + row = append(row, slots[i+1]) } else { - // Добавляем пустую кнопку для выравнивания row = append(row, bot.Button(" ", "empty")) } - buttons = append(buttons, row) } // Кнопки пагинации с номером страницы посередине (только если есть закупы) - var navRow []echotron.InlineKeyboardButton - - // Левая стрелка (активная или неактивная) - if s.CurrentPage > 0 { - navRow = append(navRow, bot.Button("←", "prev")) - } else { - navRow = append(navRow, bot.Button("·", "empty")) + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.CurrentPage, + TotalPages: page.Pages, + MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, page.Pages), "page_info")}, + }); navRow != nil { + buttons = append(buttons, navRow) } - - // Кнопка с номером страницы (неактивная) - pageButton := bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, page.Pages), "page_info") - navRow = append(navRow, pageButton) - - // Правая стрелка (активная или неактивная) - if s.CurrentPage+1 < page.Pages { - navRow = append(navRow, bot.Button("→", "next")) - } else { - navRow = append(navRow, bot.Button("·", "empty")) - } - - buttons = append(buttons, navRow) } // Кнопки управления diff --git a/telegram_bot/screens/my_projects.go b/telegram_bot/screens/my_projects.go index f27c924..6047e1a 100644 --- a/telegram_bot/screens/my_projects.go +++ b/telegram_bot/screens/my_projects.go @@ -6,6 +6,7 @@ import ( "strings" "example.com/m/bot" + "example.com/m/ui" "github.com/NicoNex/echotron/v3" "github.com/rs/zerolog/log" ) @@ -107,11 +108,6 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) %s`, navTitle, headerTitle, emptyStateText) } else { - pageInfo := "" - if page.Pages > 1 { - pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages) - } - text = fmt.Sprintf(`%s %s%s @@ -119,7 +115,7 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) %s`, navTitle, headerTitle, - pageInfo, + ui.FormatPageInfo(s.CurrentPage, page.Pages), nonEmptyText) } @@ -153,23 +149,14 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) } // Ряд: Пагинация + Добавить (динамическая ширина) - var navRow []echotron.InlineKeyboardButton - - // Левая стрелка (только если есть предыдущая страница) - if s.CurrentPage > 0 { - navRow = append(navRow, bot.Button("←", "prev")) + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.CurrentPage, + TotalPages: page.Pages, + MiddleButtons: []echotron.InlineKeyboardButton{bot.Button("+ Добавить", "add_project")}, + }); navRow != nil { + buttons = append(buttons, navRow) } - // Кнопка "Добавить проект" всегда в центре - navRow = append(navRow, bot.Button("+ Добавить", "add_project")) - - // Правая стрелка (только если есть следующая страница) - if s.CurrentPage+1 < page.Pages { - navRow = append(navRow, bot.Button("→", "next")) - } - - buttons = append(buttons, navRow) - // Нижний ряд: Назад + Архив buttons = append(buttons, bot.Row( bot.Button("← Назад", "menu"), diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go index 51c8438..8c2717a 100644 --- a/telegram_bot/screens/purchase_optional_details.go +++ b/telegram_bot/screens/purchase_optional_details.go @@ -7,6 +7,7 @@ import ( "time" "example.com/m/bot" + "example.com/m/ui" "github.com/NicoNex/echotron/v3" ) @@ -555,15 +556,10 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende return } - start := s.ParamPage * perPage - if start >= total { + if s.ParamPage*perPage >= total { s.ParamPage = 0 - start = 0 - } - end := start + perPage - if end > total { - end = total } + start, end := ui.GetPageBounds(s.ParamPage, perPage, total) var rows [][]echotron.InlineKeyboardButton for i := start; i < end; i++ { @@ -577,20 +573,11 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende )) } - pages := (total + perPage - 1) / perPage - if pages > 1 { - var navRow []echotron.InlineKeyboardButton - if s.ParamPage > 0 { - navRow = append(navRow, bot.Button("←", "param_prev")) - } else { - navRow = append(navRow, bot.Button("·", "empty")) - } - navRow = append(navRow, bot.Button(fmt.Sprintf("• %d/%d •", s.ParamPage+1, pages), "page_info")) - if s.ParamPage+1 < pages { - navRow = append(navRow, bot.Button("→", "param_next")) - } else { - navRow = append(navRow, bot.Button("·", "empty")) - } + pages := ui.CalculatePages(total, perPage) + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.ParamPage, + TotalPages: pages, + }); navRow != nil { rows = append(rows, navRow) } @@ -688,13 +675,13 @@ func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) b s.applyParamToAll(param) s.Enter(b, bot.EditMessage) return true - case data == "param_prev": + case data == "prev" && strings.HasSuffix(s.InputMode, "_channels"): if s.ParamPage > 0 { s.ParamPage-- } s.Enter(b, bot.EditMessage) return true - case data == "param_next": + case data == "next" && strings.HasSuffix(s.InputMode, "_channels"): s.ParamPage++ s.Enter(b, bot.EditMessage) return true diff --git a/telegram_bot/screens/select_channels_for_purchase.go b/telegram_bot/screens/select_channels_for_purchase.go index 93bb92d..cb58908 100644 --- a/telegram_bot/screens/select_channels_for_purchase.go +++ b/telegram_bot/screens/select_channels_for_purchase.go @@ -8,6 +8,7 @@ import ( "example.com/m/adapter/backend" "example.com/m/bot" + "example.com/m/ui" "github.com/NicoNex/echotron/v3" "github.com/rs/zerolog/log" ) @@ -63,15 +64,10 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot. text += "\n" const channelsPerPage = 6 - start := s.CurrentPage * channelsPerPage - if start >= len(s.Channels) { + if s.CurrentPage*channelsPerPage >= len(s.Channels) { s.CurrentPage = 0 - start = 0 - } - end := start + channelsPerPage - if end > len(s.Channels) { - end = len(s.Channels) } + start, end := ui.GetPageBounds(s.CurrentPage, channelsPerPage, len(s.Channels)) var slots []echotron.InlineKeyboardButton for i := start; i < end; i++ { @@ -99,20 +95,12 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot. buttons = append(buttons, row) } - pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage - if pages > 1 { - var navRow []echotron.InlineKeyboardButton - if s.CurrentPage > 0 { - navRow = append(navRow, bot.Button("←", "prev_page")) - } else { - navRow = append(navRow, bot.Button("·", "empty")) - } - navRow = append(navRow, bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, pages), "page_info")) - if s.CurrentPage+1 < pages { - navRow = append(navRow, bot.Button("→", "next_page")) - } else { - navRow = append(navRow, bot.Button("·", "empty")) - } + pages := ui.CalculatePages(len(s.Channels), channelsPerPage) + navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.CurrentPage, + TotalPages: pages, + }) + if navRow != nil { buttons = append(buttons, navRow) } @@ -130,7 +118,7 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot. if len(s.Channels) > 0 { buttons = append(buttons, bot.Row( bot.Button("← Назад", "back"), - bot.Button("→ Далее", "next"), + bot.Button("→ Далее", "next_step"), )) } else { buttons = append(buttons, bot.Row( @@ -176,7 +164,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat BackState: s.BackState, }, bot.EditMessage) - case "next": + case "next_step": if len(s.Channels) == 0 { b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard( bot.Row(bot.Button("← Назад", "back_to_select")), @@ -197,13 +185,13 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat case "back_to_select": s.Enter(b, bot.EditMessage) - case "prev_page": + case "prev": if s.CurrentPage > 0 { s.CurrentPage-- } s.Enter(b, bot.EditMessage) - case "next_page": + case "next": s.CurrentPage++ s.Enter(b, bot.EditMessage) @@ -215,7 +203,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat s.Channels = append(s.Channels[:index], s.Channels[index+1:]...) if s.CurrentPage > 0 { channelsPerPage := 6 - pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage + pages := ui.CalculatePages(len(s.Channels), channelsPerPage) if s.CurrentPage >= pages { s.CurrentPage = pages - 1 } @@ -262,7 +250,8 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) { } // Показываем успешное создание - successText := fmt.Sprintf(`… › Создание › Готово + successText := fmt.Sprintf(` +… › Создание › Готово Закуп создан @@ -270,7 +259,8 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) { ▪ Креатив: %s ▪ Каналов: %d -Закуп создан и готов к работе`, purchase.ID, purchase.CreativeID, len(purchase.Channels)) +Закуп создан и готов к работе +`, purchase.ID, purchase.CreativeID, len(purchase.Channels)) b.Edit(successText, bot.Keyboard( bot.Row(bot.Button("● Готово", "done")), diff --git a/telegram_bot/ui/pagination.go b/telegram_bot/ui/pagination.go new file mode 100644 index 0000000..45c5bb3 --- /dev/null +++ b/telegram_bot/ui/pagination.go @@ -0,0 +1,93 @@ +package ui + +import ( + "fmt" + + "example.com/m/bot" + "github.com/NicoNex/echotron/v3" +) + +// PaginationConfig конфигурация для построения навигационного ряда пагинации +type PaginationConfig struct { + CurrentPage int + TotalPages int // Для API пагинации (из page.Pages) + TotalItems int // Для локальной пагинации (len(array)) + ItemsPerPage int // Для расчета TotalPages из TotalItems + PrevCallback string // По умолчанию "prev" + NextCallback string // По умолчанию "next" + MiddleButtons []echotron.InlineKeyboardButton // Опциональные кнопки в центре (например, "+ Добавить") +} + +// BuildNavigationRow создает навигационный ряд с динамической шириной. +// Показывает ряд только если страниц > 1. +// Возвращает nil если пагинация не нужна. +func BuildNavigationRow(config PaginationConfig) []echotron.InlineKeyboardButton { + // Вычисляем TotalPages из TotalItems если не задан + totalPages := config.TotalPages + if totalPages == 0 && config.TotalItems > 0 && config.ItemsPerPage > 0 { + totalPages = CalculatePages(config.TotalItems, config.ItemsPerPage) + } + + // Устанавливаем значения по умолчанию для callbacks + prevCallback := config.PrevCallback + if prevCallback == "" { + prevCallback = "prev" + } + nextCallback := config.NextCallback + if nextCallback == "" { + nextCallback = "next" + } + + var row []echotron.InlineKeyboardButton + + // Левая стрелка (только если есть предыдущая страница) + if totalPages > 1 && config.CurrentPage > 0 { + row = append(row, bot.Button("←", prevCallback)) + } + + // Средние кнопки (если есть) + if len(config.MiddleButtons) > 0 { + row = append(row, config.MiddleButtons...) + } + + // Правая стрелка (только если есть следующая страница) + if totalPages > 1 && config.CurrentPage+1 < totalPages { + row = append(row, bot.Button("→", nextCallback)) + } + + if len(row) == 0 { + return nil + } + + return row +} + +// CalculatePages вычисляет количество страниц из общего количества элементов +func CalculatePages(totalItems, itemsPerPage int) int { + if itemsPerPage <= 0 { + return 0 + } + return (totalItems + itemsPerPage - 1) / itemsPerPage +} + +// GetPageBounds возвращает start/end индексы для текущей страницы. +// Автоматически сбрасывает на первую страницу если currentPage выходит за границы. +func GetPageBounds(currentPage, itemsPerPage, totalItems int) (start, end int) { + start = currentPage * itemsPerPage + if start >= totalItems { + start = 0 + } + end = start + itemsPerPage + if end > totalItems { + end = totalItems + } + return start, end +} + +// FormatPageInfo форматирует текст индикатора страницы для заголовка +func FormatPageInfo(currentPage, totalPages int) string { + if totalPages <= 1 { + return "" + } + return fmt.Sprintf(" (стр. %d/%d)", currentPage+1, totalPages) +}