Files
tgex-backend/tg_bot/screens/my_projects.go
2026-01-20 13:40:24 +03:00

235 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package screens
import (
"context"
"fmt"
"strings"
"time"
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
"github.com/rs/zerolog/log"
)
const projectsPerPage = 6
type MyProjects struct {
CurrentPage int
BackState bot.State
OpenPlacements bool
// Поля для управления фоновой горутиной polling
cancel context.CancelFunc
lastProjectCount int
}
func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
s.renderProjects(b, mode)
s.startPolling(b)
}
func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode) {
page, err := b.Backend.GetProjects(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CurrentPage+1, projectsPerPage)
if err != nil {
log.Error().Err(err).Msg("Failed to get projects")
b.SendNew("❌ Не удалось загрузить проекты", Keyboard())
return
}
// Сохраняем количество проектов для polling
s.lastProjectCount = page.Total
var text string
var buttons [][]echotron.InlineKeyboardButton
headerTitle := "Мои проекты"
emptyStateText := `У вас пока нет подключенных каналов.
<i>Что дает подключение канала:</i>
▸ Управление типом приглашений (публичные/с одобрением)
▸ Автоматическая отправка креативов в канал
▸ Интеграция с планом закупов
▸ Статистика и аналитика подписчиков
▸ Настройка уведомлений
Начните с добавления первого проекта`
nonEmptyText := "Управление вашими Telegram-каналами.\n\nВыберите канал"
if s.OpenPlacements {
headerTitle = "Размещения"
nonEmptyText = "Выберите проект для просмотра закупов"
}
if len(page.Items) == 0 {
text = fmt.Sprintf(`<b>%s</b>
%s`, headerTitle, emptyStateText)
} else {
text = fmt.Sprintf(`<b>%s</b>%s
%s`,
headerTitle,
ui.FormatPageInfo(s.CurrentPage, page.Pages),
nonEmptyText)
}
// Создаем слоты для проектов
var slots []echotron.InlineKeyboardButton
for _, project := range page.Items {
slots = append(slots, Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
}
// Если страниц >= 2, заполняем до 6 слотов пустыми кнопками
if page.Pages >= 2 {
for len(slots) < projectsPerPage {
slots = append(slots, Button(" ", "empty"))
}
}
// Разбиваем слоты по 2 в ряду
for i := 0; i < len(slots); i += 2 {
var row []echotron.InlineKeyboardButton
row = append(row, slots[i])
// Второй слот в ряду (если есть)
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
// Если нечетное количество, добавляем пустую кнопку
row = append(row, Button(" ", "empty"))
}
buttons = append(buttons, row)
}
// Ряд: Пагинация + Добавить (динамическая ширина)
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
TotalPages: page.Pages,
MiddleButtons: []echotron.InlineKeyboardButton{Button(" Добавить", "add_project")},
}); navRow != nil {
buttons = append(buttons, navRow)
}
// Нижний ряд: Назад + Архив
buttons = append(buttons, Row(
Button("← Назад", "main_menu"),
Button("≡ Архив", "archive"),
))
keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch {
case data == "prev":
if s.CurrentPage > 0 {
s.CurrentPage--
}
s.Enter(b, bot.EditMessage)
case data == "next":
s.CurrentPage++
s.Enter(b, bot.EditMessage)
case data == "main_menu":
b.SetState(&MainMenu{}, bot.EditMessage)
case data == "archive":
b.Edit("📦 Архив проектов\n\nЭта функция в разработке", Keyboard(
Row(Button("← Назад", "back_to_projects")),
))
case data == "add_project":
b.SetState(&AddProject{BackState: &MyProjects{}}, bot.EditMessage)
case data == "back_to_projects":
s.Enter(b, bot.EditMessage)
case strings.HasPrefix(data, "project:"):
projectID, ok := strings.CutPrefix(data, "project:")
if !ok || projectID == "" {
return
}
project, err := b.Backend.GetProject(context.Background(), b.Session.JWT, b.Session.WorkspaceID, projectID)
if err != nil {
log.Error().Err(err).Str("project_id", projectID).Msg("Failed to get project")
return
}
if s.OpenPlacements {
b.SetState(&Placements{
ProjectID: project.ID,
ProjectTitle: project.Title,
BackState: &MyProjects{OpenPlacements: true},
}, bot.EditMessage)
} else {
b.SetState(&ProjectDetails{
Project: project,
BackState: &MyProjects{},
}, bot.EditMessage)
}
default:
s.Enter(b, bot.NewMessage)
}
return
}
func (s *MyProjects) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *MyProjects) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *MyProjects) Exit() {
if s.cancel != nil {
log.Info().Msg("MyProjects: cancelling polling goroutine")
s.cancel()
}
}
func (s *MyProjects) startPolling(b *bot.Bot) {
if s.cancel != nil {
s.cancel()
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
s.cancel = cancel
go s.pollProjects(ctx, b)
}
func (s *MyProjects) pollProjects(ctx context.Context, b *bot.Bot) {
interval := 3 * time.Second
timeToSlow := time.Now().Add(30 * time.Second)
timer := time.NewTimer(interval)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
if time.Now().After(timeToSlow) {
interval = 10 * time.Second
}
page, err := b.Backend.GetProjects(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CurrentPage+1, projectsPerPage)
if err == nil && page.Total != s.lastProjectCount {
s.lastProjectCount = page.Total
s.renderProjects(b, bot.EditMessage)
}
timer.Reset(interval)
}
}
}