обработка обновлений последовательно для каждого юзера благодаря mutex

This commit is contained in:
Artem Tsyrulnikov
2026-01-04 19:35:41 +03:00
parent 5251ec3015
commit 592ade7dce
9 changed files with 235 additions and 190 deletions

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"sync"
"example.com/m/adapter/backend" "example.com/m/adapter/backend"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
@@ -37,6 +38,7 @@ type Exec struct {
type Bot struct { type Bot struct {
echotron.API echotron.API
mu sync.Mutex
ChatID int64 ChatID int64
exec *Exec exec *Exec
CurrentState State CurrentState State
@@ -205,6 +207,9 @@ func (b *Bot) SendMessageWithURLButton(text, buttonText, url string) {
} }
func (b *Bot) Update(u *echotron.Update) { func (b *Bot) Update(u *echotron.Update) {
b.mu.Lock()
defer b.mu.Unlock()
log.Info(). log.Info().
Int64("chat_id", b.ChatID). Int64("chat_id", b.ChatID).
Str("current_state", fmt.Sprintf("%T", b.CurrentState)). Str("current_state", fmt.Sprintf("%T", b.CurrentState)).

View File

@@ -12,6 +12,11 @@ type AcceptWorkspaceInvite struct {
InviteID string InviteID string
} }
const msgAcceptWorkspaceInviteSuccessfully = `
✅ Вы присоединились к рабочему пространству!
Администратор сможет выдать вам необходимые права в веб-интерфейсе.
`
func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) {
// Получаем JWT токен // Получаем JWT токен
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID) jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
@@ -29,11 +34,7 @@ func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) {
return return
} }
// Успешно принято b.SendNew(msgAcceptWorkspaceInviteSuccessfully, bot.Keyboard(
text := "✅ Вы присоединились к рабочему пространству!\n\n" +
"Администратор сможет выдать вам необходимые права в веб-интерфейсе."
b.SendNew(text, bot.Keyboard(
bot.Row(bot.Button("🏠 Главное меню", "menu")), bot.Row(bot.Button("🏠 Главное меню", "menu")),
)) ))
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"example.com/m/bot" "example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log" "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 { if len(page.Items) == 0 {
text += "У вас нет проектов. Создайте проект и попробуйте снова.\n\n" text += "У вас нет проектов. Создайте проект и попробуйте снова.\n\n"
} else { } else {
pageInfo := "" text += fmt.Sprintf("Проекты%s\n\n", ui.FormatPageInfo(s.ProjectPage, page.Pages))
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text += fmt.Sprintf("Проекты%s\n\n", pageInfo) var slots []echotron.InlineKeyboardButton
for _, project := range page.Items { for _, project := range page.Items {
buttons = append(buttons, bot.Row( slots = append(slots, bot.Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
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 navRow := ui.BuildNavigationRow(ui.PaginationConfig{
if s.ProjectPage > 0 { CurrentPage: s.ProjectPage,
navRow = append(navRow, bot.Button("←", "prev_project")) TotalPages: page.Pages,
} }); navRow != nil {
if s.ProjectPage+1 < page.Pages {
navRow = append(navRow, bot.Button("→", "next_project"))
}
if len(navRow) > 0 {
buttons = append(buttons, navRow) buttons = append(buttons, navRow)
} }
} }
@@ -146,27 +152,31 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
Вернитесь назад и создайте креатив в разделе <b>Креативы</b>` Вернитесь назад и создайте креатив в разделе <b>Креативы</b>`
} else { } else {
pageInfo := "" text += fmt.Sprintf("Креативы%s\n\n", ui.FormatPageInfo(s.CreativePage, page.Pages))
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text += fmt.Sprintf("Креативы%s\n\n", pageInfo)
var slots []echotron.InlineKeyboardButton
for _, creative := range page.Items { for _, creative := range page.Items {
buttons = append(buttons, bot.Row( slots = append(slots, bot.Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
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 navRow := ui.BuildNavigationRow(ui.PaginationConfig{
if s.CreativePage > 0 { CurrentPage: s.CreativePage,
navRow = append(navRow, bot.Button("←", "prev_creative")) TotalPages: page.Pages,
} }); navRow != nil {
if s.CreativePage+1 < page.Pages {
navRow = append(navRow, bot.Button("→", "next_creative"))
}
if len(navRow) > 0 {
buttons = append(buttons, navRow) buttons = append(buttons, navRow)
} }
} }
@@ -232,27 +242,33 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.ActivePicker = "" s.ActivePicker = ""
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "prev_project": case "prev":
if s.ActivePicker == "project_list" {
if s.ProjectPage > 0 { if s.ProjectPage > 0 {
s.ProjectPage-- s.ProjectPage--
} }
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return
case "next_project": }
s.ProjectPage++ if s.ActivePicker == "creative_list" {
s.Enter(b, bot.EditMessage)
case "prev_creative":
if s.CreativePage > 0 { if s.CreativePage > 0 {
s.CreativePage-- s.CreativePage--
} }
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return
case "next_creative": }
s.CreativePage++
s.Enter(b, bot.EditMessage)
case "next": 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 != "" { if s.ProjectID != "" && s.CreativeID != "" {
b.SetState(&SelectChannelsForPurchase{ b.SetState(&SelectChannelsForPurchase{
WorkspaceID: s.WorkspaceID, WorkspaceID: s.WorkspaceID,

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"example.com/m/bot" "example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -71,11 +72,6 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
Начните с добавления первого креатива`, navPath) Начните с добавления первого креатива`, navPath)
} else { } else {
pageInfo := ""
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text = fmt.Sprintf(`<i>%s</i> text = fmt.Sprintf(`<i>%s</i>
<b>Креативы</b>%s <b>Креативы</b>%s
@@ -84,7 +80,7 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
Выберите креатив`, Выберите креатив`,
navPath, navPath,
pageInfo) ui.FormatPageInfo(s.CurrentPage, page.Pages))
} }
// Создаем слоты для креативов // Создаем слоты для креативов
@@ -117,22 +113,13 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
} }
// Ряд: Пагинация + Добавить (динамическая ширина) // Ряд: Пагинация + Добавить (динамическая ширина)
var navRow []echotron.InlineKeyboardButton if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
// Левая стрелка (только если есть предыдущая страница) TotalPages: page.Pages,
if s.CurrentPage > 0 { MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(" Добавить", "add_creative")},
navRow = append(navRow, bot.Button("←", "prev")) }); navRow != nil {
}
// Кнопка "Добавить креатив" всегда в центре
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, navRow)
}
// Нижний ряд: Назад + Архив // Нижний ряд: Назад + Архив
buttons = append(buttons, bot.Row( buttons = append(buttons, bot.Row(

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"example.com/m/bot" "example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -75,63 +76,41 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
Размещения создаются автоматически при создании закупа` Размещения создаются автоматически при создании закупа`
} else { } else {
pageInfo := "" text += fmt.Sprintf("Выберите размещение для деталей%s\n\n", ui.FormatPageInfo(s.CurrentPage, page.Pages))
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", s.CurrentPage+1, page.Pages)
}
text += fmt.Sprintf("Выберите размещение для деталей%s\n\n", pageInfo)
// Размещения по 2 в ряду // Размещения по 2 в ряду
for i := 0; i < len(page.Items); i += 2 { var slots []echotron.InlineKeyboardButton
var row []echotron.InlineKeyboardButton for _, placement := range page.Items {
// Первый placement в ряду
placement := page.Items[i]
placementName := placement.CreativeName placementName := placement.CreativeName
if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" { if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" {
placementName = fmt.Sprintf("%s → %s", placement.CreativeName, *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)))
// Второй 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))) 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 { } else {
// Добавляем пустую кнопку для выравнивания
row = append(row, bot.Button(" ", "empty")) row = append(row, bot.Button(" ", "empty"))
} }
buttons = append(buttons, row) buttons = append(buttons, row)
} }
// Кнопки пагинации с номером страницы посередине (только если есть закупы) // Кнопки пагинации с номером страницы посередине (только если есть закупы)
var navRow []echotron.InlineKeyboardButton if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
// Левая стрелка (активная или неактивная) TotalPages: page.Pages,
if s.CurrentPage > 0 { MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, page.Pages), "page_info")},
navRow = append(navRow, bot.Button("←", "prev")) }); navRow != nil {
} else {
navRow = append(navRow, bot.Button("·", "empty"))
}
// Кнопка с номером страницы (неактивная)
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) buttons = append(buttons, navRow)
} }
}
// Кнопки управления // Кнопки управления
buttons = append(buttons, bot.Row( buttons = append(buttons, bot.Row(

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"example.com/m/bot" "example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log" "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) %s`, navTitle, headerTitle, emptyStateText)
} else { } else {
pageInfo := ""
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text = fmt.Sprintf(`<i>%s</i> text = fmt.Sprintf(`<i>%s</i>
<b>%s</b>%s <b>%s</b>%s
@@ -119,7 +115,7 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
%s`, %s`,
navTitle, navTitle,
headerTitle, headerTitle,
pageInfo, ui.FormatPageInfo(s.CurrentPage, page.Pages),
nonEmptyText) nonEmptyText)
} }
@@ -153,22 +149,13 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
} }
// Ряд: Пагинация + Добавить (динамическая ширина) // Ряд: Пагинация + Добавить (динамическая ширина)
var navRow []echotron.InlineKeyboardButton if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
// Левая стрелка (только если есть предыдущая страница) TotalPages: page.Pages,
if s.CurrentPage > 0 { MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(" Добавить", "add_project")},
navRow = append(navRow, bot.Button("←", "prev")) }); navRow != nil {
}
// Кнопка "Добавить проект" всегда в центре
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, navRow)
}
// Нижний ряд: Назад + Архив // Нижний ряд: Назад + Архив
buttons = append(buttons, bot.Row( buttons = append(buttons, bot.Row(

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"example.com/m/bot" "example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
) )
@@ -555,15 +556,10 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende
return return
} }
start := s.ParamPage * perPage if s.ParamPage*perPage >= total {
if start >= total {
s.ParamPage = 0 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 var rows [][]echotron.InlineKeyboardButton
for i := start; i < end; i++ { 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 pages := ui.CalculatePages(total, perPage)
if pages > 1 { if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
var navRow []echotron.InlineKeyboardButton CurrentPage: s.ParamPage,
if s.ParamPage > 0 { TotalPages: pages,
navRow = append(navRow, bot.Button("←", "param_prev")) }); navRow != nil {
} 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"))
}
rows = append(rows, navRow) rows = append(rows, navRow)
} }
@@ -688,13 +675,13 @@ func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) b
s.applyParamToAll(param) s.applyParamToAll(param)
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return true return true
case data == "param_prev": case data == "prev" && strings.HasSuffix(s.InputMode, "_channels"):
if s.ParamPage > 0 { if s.ParamPage > 0 {
s.ParamPage-- s.ParamPage--
} }
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return true return true
case data == "param_next": case data == "next" && strings.HasSuffix(s.InputMode, "_channels"):
s.ParamPage++ s.ParamPage++
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return true return true

View File

@@ -8,6 +8,7 @@ import (
"example.com/m/adapter/backend" "example.com/m/adapter/backend"
"example.com/m/bot" "example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -63,15 +64,10 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
text += "\n" text += "\n"
const channelsPerPage = 6 const channelsPerPage = 6
start := s.CurrentPage * channelsPerPage if s.CurrentPage*channelsPerPage >= len(s.Channels) {
if start >= len(s.Channels) {
s.CurrentPage = 0 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 var slots []echotron.InlineKeyboardButton
for i := start; i < end; i++ { for i := start; i < end; i++ {
@@ -99,20 +95,12 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
buttons = append(buttons, row) buttons = append(buttons, row)
} }
pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage pages := ui.CalculatePages(len(s.Channels), channelsPerPage)
if pages > 1 { navRow := ui.BuildNavigationRow(ui.PaginationConfig{
var navRow []echotron.InlineKeyboardButton CurrentPage: s.CurrentPage,
if s.CurrentPage > 0 { TotalPages: pages,
navRow = append(navRow, bot.Button("←", "prev_page")) })
} else { if navRow != nil {
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"))
}
buttons = append(buttons, navRow) buttons = append(buttons, navRow)
} }
@@ -130,7 +118,7 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
if len(s.Channels) > 0 { if len(s.Channels) > 0 {
buttons = append(buttons, bot.Row( buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"), bot.Button("← Назад", "back"),
bot.Button("→ Далее", "next"), bot.Button("→ Далее", "next_step"),
)) ))
} else { } else {
buttons = append(buttons, bot.Row( buttons = append(buttons, bot.Row(
@@ -176,7 +164,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
BackState: s.BackState, BackState: s.BackState,
}, bot.EditMessage) }, bot.EditMessage)
case "next": case "next_step":
if len(s.Channels) == 0 { if len(s.Channels) == 0 {
b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard( b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")), 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": case "back_to_select":
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "prev_page": case "prev":
if s.CurrentPage > 0 { if s.CurrentPage > 0 {
s.CurrentPage-- s.CurrentPage--
} }
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "next_page": case "next":
s.CurrentPage++ s.CurrentPage++
s.Enter(b, bot.EditMessage) 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:]...) s.Channels = append(s.Channels[:index], s.Channels[index+1:]...)
if s.CurrentPage > 0 { if s.CurrentPage > 0 {
channelsPerPage := 6 channelsPerPage := 6
pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage pages := ui.CalculatePages(len(s.Channels), channelsPerPage)
if s.CurrentPage >= pages { if s.CurrentPage >= pages {
s.CurrentPage = pages - 1 s.CurrentPage = pages - 1
} }
@@ -262,7 +250,8 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
} }
// Показываем успешное создание // Показываем успешное создание
successText := fmt.Sprintf(`<i>… Создание Готово</i> successText := fmt.Sprintf(`
<i>… Создание Готово</i>
<b>Закуп создан</b> <b>Закуп создан</b>
@@ -270,7 +259,8 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
▪ <b>Креатив:</b> %s ▪ <b>Креатив:</b> %s
▪ <b>Каналов:</b> %d ▪ <b>Каналов:</b> %d
Закуп создан и готов к работе`, purchase.ID, purchase.CreativeID, len(purchase.Channels)) Закуп создан и готов к работе
`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
b.Edit(successText, bot.Keyboard( b.Edit(successText, bot.Keyboard(
bot.Row(bot.Button("● Готово", "done")), bot.Row(bot.Button("● Готово", "done")),

View File

@@ -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(" <i>(стр. %d/%d)</i>", currentPage+1, totalPages)
}