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 := `У вас пока нет подключенных каналов.
Что дает подключение канала:
▸ Управление типом приглашений (публичные/с одобрением)
▸ Автоматическая отправка креативов в канал
▸ Интеграция с планом закупов
▸ Статистика и аналитика подписчиков
▸ Настройка уведомлений
Начните с добавления первого проекта`
nonEmptyText := "Управление вашими Telegram-каналами.\n\nВыберите канал"
if s.OpenPlacements {
headerTitle = "Размещения"
nonEmptyText = "Выберите проект для просмотра закупов"
}
if len(page.Items) == 0 {
text = fmt.Sprintf(`%s
%s`, headerTitle, emptyStateText)
} else {
text = fmt.Sprintf(`%s%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)
}
}
}