diff --git a/tg_bot/backend/backend_client.go b/tg_bot/backend/backend_client.go index 6a79496..512b926 100644 --- a/tg_bot/backend/backend_client.go +++ b/tg_bot/backend/backend_client.go @@ -206,11 +206,7 @@ func (c *Client) GetWorkspaces( return resp.Items, err } -func (c *Client) CreateWorkspace( - ctx context.Context, - jwt string, - name string, -) (*Workspace, error) { +func (c *Client) CreateWorkspace(ctx context.Context, jwt string, name string) (Workspace, error) { req := struct { Name string `json:"name"` }{ @@ -218,16 +214,9 @@ func (c *Client) CreateWorkspace( } var workspace Workspace - err := c.do( - ctx, - http.MethodPost, - "api/v1/workspaces", - req, - &workspace, - withBearer(jwt), - ) + err := c.do(ctx, http.MethodPost, "api/v1/workspaces", req, &workspace, withBearer(jwt)) - return &workspace, err + return workspace, err } func (c *Client) GetProjects( diff --git a/tg_bot/bot/state.go b/tg_bot/bot/state.go index 343cc39..7c9d7c8 100644 --- a/tg_bot/bot/state.go +++ b/tg_bot/bot/state.go @@ -18,6 +18,7 @@ type State interface { } type Session struct { - FirstName string - JWT string + FirstName string + JWT string + WorkspaceID string } diff --git a/tg_bot/screens/accept_workspace_invite.go b/tg_bot/screens/accept_workspace_invite.go index 2131fab..1c70009 100644 --- a/tg_bot/screens/accept_workspace_invite.go +++ b/tg_bot/screens/accept_workspace_invite.go @@ -29,9 +29,7 @@ func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) { return } - kb := Keyboard( - Row(Button("Главное меню", "menu")), - ) + kb := Keyboard(Row(Button("Главное меню", "main_menu"))) b.SendNew(msgAcceptWorkspaceInviteSuccessfully, kb) } diff --git a/tg_bot/screens/add_creative.go b/tg_bot/screens/add_creative.go index ad71780..18b3c4b 100644 --- a/tg_bot/screens/add_creative.go +++ b/tg_bot/screens/add_creative.go @@ -17,9 +17,8 @@ import ( type AddCreativeCtx struct { CreativeEditorFields - ProjectID string - WorkspaceID string - BackState bot.State + ProjectID string + BackState bot.State } type AddCreativeStart struct{ ctx *AddCreativeCtx } @@ -373,7 +372,7 @@ func (s *AddCreativeCtx) createCreative(b *bot.Bot) { log.Info().Str("text", *s.Text).Msg("Creative text") - creative, err := b.Backend.CreateCreative(context.Background(), b.Session.JWT, s.WorkspaceID, s.ProjectID, backend.CreateCreativeInput{ + creative, err := b.Backend.CreateCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, backend.CreateCreativeInput{ Name: *s.Name, Text: *s.Text, MediaType: s.MediaType, diff --git a/tg_bot/screens/add_purchase.go b/tg_bot/screens/add_purchase.go index af19784..bebc5f9 100644 --- a/tg_bot/screens/add_purchase.go +++ b/tg_bot/screens/add_purchase.go @@ -14,7 +14,6 @@ const addPurchaseCreativesPerPage = 5 const addPurchaseProjectsPerPage = 6 type AddPurchase struct { - WorkspaceID string ProjectID string ProjectTitle string ProjectTelegramID int64 @@ -42,18 +41,10 @@ type creativeInfo struct { } func (s *AddPurchase) Enter(b *bot.Bot, mode bot.RenderMode) { - // JWT уже создан в Bot.Update(), просто берем из сессии - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard()) - return - } - - s.renderSelection(b, mode, jwt) + s.renderSelection(b, mode) } -func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt string) { +func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) { text := "Создание закупа\n\n" text += "Выберите проект и креатив.\n\n" @@ -79,13 +70,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin switch s.ActivePicker { case "project_list": - page, err := b.Backend.GetProjects( - context.Background(), - jwt, - s.WorkspaceID, - s.ProjectPage+1, - addPurchaseProjectsPerPage, - ) + page, err := b.Backend.GetProjects(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectPage+1, addPurchaseProjectsPerPage) if err != nil { log.Error().Err(err).Msg("Failed to get projects") b.SendNew("❌ Не удалось загрузить проекты", Keyboard()) @@ -151,15 +136,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin break } - page, err := b.Backend.GetCreatives( - context.Background(), - jwt, - s.WorkspaceID, - &s.ProjectID, - false, - s.CreativePage+1, - addPurchaseCreativesPerPage, - ) + page, err := b.Backend.GetCreatives(context.Background(), b.Session.JWT, b.Session.WorkspaceID, &s.ProjectID, false, s.CreativePage+1, addPurchaseCreativesPerPage) if err != nil { log.Error().Err(err).Msg("Failed to get creatives") b.SendNew("❌ Не удалось загрузить креативы", Keyboard()) @@ -288,7 +265,6 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) { } if s.ProjectID != "" && s.CreativeID != "" { b.SetState(&SelectChannelsForPurchase{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, ProjectTitle: s.ProjectTitle, ProjectTelegramID: s.ProjectTelegramID, diff --git a/tg_bot/screens/creative_details.go b/tg_bot/screens/creative_details.go index b6e8373..75364ad 100644 --- a/tg_bot/screens/creative_details.go +++ b/tg_bot/screens/creative_details.go @@ -15,17 +15,16 @@ import ( type CreativeDetails struct { CreativeEditorFields // Встраивание общих полей - CreativeID string - ProjectID string - WorkspaceID string - BackState bot.State + CreativeID string + ProjectID string + BackState bot.State // Флаг подтверждения удаления confirmingDelete bool } func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) { - creative, err := b.Backend.GetCreative(context.Background(), b.Session.JWT, s.WorkspaceID, s.CreativeID) + creative, err := b.Backend.GetCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CreativeID) if err != nil { log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative") b.SendNew("❌ Не удалось загрузить креатив", Keyboard( @@ -313,13 +312,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) { input.Buttons = &buttons // Отправляем запрос на обновление - _, err = b.Backend.UpdateCreative( - context.Background(), - jwt, - s.WorkspaceID, - s.CreativeID, - input, - ) + _, err = b.Backend.UpdateCreative(context.Background(), jwt, b.Session.WorkspaceID, s.CreativeID, input) if err != nil { log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to update creative") @@ -341,7 +334,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) { } func (s *CreativeDetails) deleteCreative(b *bot.Bot) { - err := b.Backend.DeleteCreative(context.Background(), b.Session.JWT, s.WorkspaceID, s.CreativeID) + err := b.Backend.DeleteCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CreativeID) if err != nil { s.confirmingDelete = false // Сбрасываем флаг подтверждения diff --git a/tg_bot/screens/creatives.go b/tg_bot/screens/creatives.go index ef6aded..76b3f46 100644 --- a/tg_bot/screens/creatives.go +++ b/tg_bot/screens/creatives.go @@ -20,32 +20,15 @@ type Creatives struct { ProjectTelegramID int64 ProjectUsername string ProjectStatus string - WorkspaceID string BackState bot.State } func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) { - // JWT уже создан в Bot.Update(), просто берем из сессии - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", Keyboard()) - return - } - - s.renderCreatives(b, mode, jwt) + s.renderCreatives(b, mode) } -func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) { - page, err := b.Backend.GetCreatives( - context.Background(), - jwt, - s.WorkspaceID, - &s.ProjectID, - false, // includeArchived - s.CurrentPage+1, - creativesPerPage, - ) +func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode) { + page, err := b.Backend.GetCreatives(context.Background(), b.Session.JWT, b.Session.WorkspaceID, &s.ProjectID, false, s.CurrentPage+1, creativesPerPage) if err != nil { log.Error().Err(err).Msg("Failed to get creatives") b.SendNew("❌ Не удалось загрузить креативы", Keyboard()) @@ -158,9 +141,8 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) { case data == "add_creative": b.SetState(&AddCreativeStart{ctx: &AddCreativeCtx{ - ProjectID: s.ProjectID, - WorkspaceID: s.WorkspaceID, - BackState: s, + ProjectID: s.ProjectID, + BackState: s, }}, bot.EditMessage) case strings.HasPrefix(data, "creative:"): @@ -170,10 +152,9 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) { creativeID := parts[1] b.SetState(&CreativeDetails{ - CreativeID: creativeID, - ProjectID: s.ProjectID, - WorkspaceID: s.WorkspaceID, - BackState: s, + CreativeID: creativeID, + ProjectID: s.ProjectID, + BackState: s, }, bot.NewMessage) } diff --git a/tg_bot/screens/main_menu.go b/tg_bot/screens/main_menu.go index 3f2e87f..6e48367 100644 --- a/tg_bot/screens/main_menu.go +++ b/tg_bot/screens/main_menu.go @@ -1,8 +1,12 @@ package screens import ( + "context" + "fmt" + "github.com/NicoNex/echotron/v3" "github.com/TelegramExchange/tgex-backend/tg_bot/bot" + "github.com/rs/zerolog/log" ) type MainMenu struct{} @@ -21,15 +25,42 @@ Smartpost — платформа для покупки и продажи рек ` func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) { + loadWorkspace := func() string { + workspaces, err := b.Backend.GetWorkspaces(context.Background(), b.Session.JWT) + if err != nil { + log.Error().Err(err).Msg("Failed to get workspaces for main menu") + return "ошибка" + } + if len(workspaces) == 0 { + workspace, err := b.Backend.CreateWorkspace(context.Background(), b.Session.JWT, "Личное") + if err != nil { + log.Error().Err(err).Msg("Failed to create workspace") + } + + workspaces = append(workspaces, workspace) + } + + selected := workspaces[0] // дефолт — первый + + if b.Session.WorkspaceID != "" { + for _, ws := range workspaces { + if ws.ID == b.Session.WorkspaceID { + selected = ws + break + } + } + } + + b.Session.WorkspaceID = selected.ID + return selected.Name + } + + workspaceName := loadWorkspace() + keyboard := Keyboard( - Row( - Button("Мои проекты", "my_projects"), - URLButton("Поддержка", "https://example.com"), - ), - Row( - Button("Размещения", "placements"), - URLButton("Дашборд", "https://example.com"), - ), + Row(Button(fmt.Sprintf("Рабочее пространство: %s", workspaceName), "workspace_menu")), + Row(Button("Мои проекты", "my_projects"), URLButton("Поддержка", "https://example.com")), + Row(Button("Размещения", "placements"), URLButton("Дашборд", "https://example.com")), ) b.Render(msgAbout, keyboard, mode) @@ -41,6 +72,8 @@ func (s *MainMenu) HandleCallback(b *bot.Bot, u *echotron.Update) { b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.EditMessage) case "placements": b.SetState(&MyProjects{BackState: &MainMenu{}, OpenPlacements: true}, bot.EditMessage) + case "workspace_menu": + b.SetState(&WorkspaceMenu{BackState: &MainMenu{}}, bot.EditMessage) default: s.Enter(b, bot.NewMessage) } diff --git a/tg_bot/screens/my_projects.go b/tg_bot/screens/my_projects.go index c9797cb..168638c 100644 --- a/tg_bot/screens/my_projects.go +++ b/tg_bot/screens/my_projects.go @@ -16,73 +16,22 @@ const projectsPerPage = 6 type MyProjects struct { CurrentPage int - WorkspaceID string BackState bot.State OpenPlacements bool // Поля для управления фоновой горутиной polling cancel context.CancelFunc - pollDone chan struct{} lastProjectCount int } func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) { - // JWT уже создан в Bot.Update(), просто берем из сессии - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", Keyboard()) - return - } + s.renderProjects(b, mode) - // Если workspace еще не выбран, получаем список workspace'ов - if s.WorkspaceID == "" { - workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt) - if err != nil { - log.Error().Err(err).Msg("Failed to get workspaces") - b.SendNew("❌ Не удалось загрузить workspace'ы", Keyboard()) - return - } - - if len(workspaces) == 0 { - // Автоматически создаем workspace с именем пользователя - username := "My Workspace" - chatInfo, err := b.GetChat(b.ChatID) - if err == nil && chatInfo.Result != nil { - if chatInfo.Result.Username != "" { - username = chatInfo.Result.Username - } else if chatInfo.Result.FirstName != "" { - username = chatInfo.Result.FirstName - if chatInfo.Result.LastName != "" { - username += " " + chatInfo.Result.LastName - } - } - } - - log.Info().Str("username", username).Msg("Creating default workspace") - workspace, err := b.Backend.CreateWorkspace(context.Background(), jwt, username) - if err != nil { - log.Error().Err(err).Msg("Failed to create workspace") - b.SendNew("❌ Не удалось создать workspace", Keyboard()) - return - } - - s.WorkspaceID = workspace.ID - log.Info().Str("workspace_id", s.WorkspaceID).Str("name", workspace.Name).Msg("Workspace created successfully") - } else { - // Используем первый workspace по умолчанию - s.WorkspaceID = workspaces[0].ID - } - } - - s.renderProjects(b, mode, jwt) - - // Запускаем фоновую горутину для автообновления экрана - s.startPolling(b, jwt) + s.startPolling(b) } -func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) { - page, err := b.Backend.GetProjects(context.Background(), jwt, s.WorkspaceID, s.CurrentPage+1, projectsPerPage) +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()) @@ -164,7 +113,7 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) // Нижний ряд: Назад + Архив buttons = append(buttons, Row( - Button("← Назад", "menu"), + Button("← Назад", "main_menu"), Button("≡ Архив", "archive"), )) @@ -190,7 +139,7 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) { s.CurrentPage++ s.Enter(b, bot.EditMessage) - case data == "menu", data == "main_menu": + case data == "main_menu": b.SetState(&MainMenu{}, bot.EditMessage) case data == "archive": @@ -199,54 +148,33 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) { )) case data == "add_project": - b.SetState(&AddProject{ - BackState: &MyProjects{WorkspaceID: s.WorkspaceID}, - }, bot.EditMessage) + b.SetState(&AddProject{BackState: &MyProjects{}}, bot.EditMessage) case data == "back_to_projects": s.Enter(b, bot.EditMessage) case strings.HasPrefix(data, "project:"): - // Открытие деталей проекта - parts := strings.Split(data, ":") - if len(parts) == 2 { - projectID := parts[1] - - // Получаем проект напрямую по ID - project, err := b.Backend.GetProject( - context.Background(), - b.Session.JWT, - s.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{ - WorkspaceID: s.WorkspaceID, - ProjectID: project.ID, - ProjectTitle: project.Title, - BackState: &MyProjects{WorkspaceID: s.WorkspaceID, OpenPlacements: true}, - }, bot.EditMessage) - } else { - b.SetState(&ProjectDetails{ - Project: project, - WorkspaceID: s.WorkspaceID, - BackState: &MyProjects{WorkspaceID: s.WorkspaceID}, - }, bot.EditMessage) - } + 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 } - case strings.HasPrefix(data, "workspace:"): - // Позволяет переключаться между workspace'ами в будущем - parts := strings.Split(data, ":") - if len(parts) == 2 { - s.WorkspaceID = parts[1] - s.CurrentPage = 0 - s.Enter(b, bot.EditMessage) + 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: @@ -260,98 +188,47 @@ func (s *MyProjects) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return } func (s *MyProjects) Handle(_ *bot.Bot, _ *echotron.Update) { return } func (s *MyProjects) Exit() { - // Останавливаем polling горутину if s.cancel != nil { log.Info().Msg("MyProjects: cancelling polling goroutine") s.cancel() } - - // Ждем завершения горутины с таймаутом 100ms - // Это не критично - горутина сама завершится при следующем tick - if s.pollDone != nil { - select { - case <-s.pollDone: - log.Info().Msg("MyProjects: polling goroutine finished") - case <-time.After(100 * time.Millisecond): - log.Info().Msg("MyProjects: polling cleanup timeout, goroutine will finish in background") - } - } } -func (s *MyProjects) startPolling(b *bot.Bot, jwt string) { - // Останавливаем предыдущую горутину если она есть +func (s *MyProjects) startPolling(b *bot.Bot) { if s.cancel != nil { s.cancel() - if s.pollDone != nil { - <-s.pollDone - } } - // Создаем новый контекст для горутины - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) s.cancel = cancel - s.pollDone = make(chan struct{}) - go s.pollProjects(ctx, b, jwt) + go s.pollProjects(ctx, b) } -func (s *MyProjects) pollProjects(ctx context.Context, b *bot.Bot, jwt string) { - defer close(s.pollDone) +func (s *MyProjects) pollProjects(ctx context.Context, b *bot.Bot) { + interval := 3 * time.Second + timeToSlow := time.Now().Add(30 * time.Second) - // Адаптивный интервал: первые 30 секунд проверяем часто (1 сек), - // потом переходим на более редкий polling (3 сек) - // Это улучшает UX когда пользователь только добавил проект - intervals := []struct { - duration time.Duration - count int - }{ - {3 * time.Second, 30}, // Первые 30 секунд - каждую секунду - {10 * time.Second, -1}, // Потом - каждые 3 секунды (бесконечно) - } - - currentIntervalIdx := 0 - tickCount := 0 - ticker := time.NewTicker(intervals[0].duration) - defer ticker.Stop() + timer := time.NewTimer(interval) + defer timer.Stop() for { select { case <-ctx.Done(): - log.Info().Msg("MyProjects polling: context cancelled, stopping") return - case <-ticker.C: - tickCount++ - - // Проверяем, нужно ли переключить интервал - if currentIntervalIdx < len(intervals)-1 { - if intervals[currentIntervalIdx].count > 0 && tickCount >= intervals[currentIntervalIdx].count { - currentIntervalIdx++ - tickCount = 0 - ticker.Reset(intervals[currentIntervalIdx].duration) - log.Info(). - Dur("new_interval", intervals[currentIntervalIdx].duration). - Msg("MyProjects polling: switching to slower interval") - } + case <-timer.C: + if time.Now().After(timeToSlow) { + interval = 10 * time.Second } - // Проверяем количество проектов - page, err := b.Backend.GetProjects(context.Background(), jwt, s.WorkspaceID, s.CurrentPage+1, projectsPerPage) - if err != nil { - log.Error().Err(err).Msg("MyProjects polling: failed to get projects") - continue - } - - // Если количество изменилось - перерендериваем - if page.Total != s.lastProjectCount { - log.Info(). - Int("old_count", s.lastProjectCount). - Int("new_count", page.Total). - Msg("MyProjects polling: project count changed, re-rendering") - + 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, jwt) + s.renderProjects(b, bot.EditMessage) } + + timer.Reset(interval) } } } diff --git a/tg_bot/screens/placement_details.go b/tg_bot/screens/placement_details.go index 14dac83..8b5bd06 100644 --- a/tg_bot/screens/placement_details.go +++ b/tg_bot/screens/placement_details.go @@ -13,31 +13,17 @@ import ( ) type PlacementDetails struct { - WorkspaceID string ProjectID string PlacementID string BackState bot.State } func (s *PlacementDetails) Enter(b *bot.Bot, mode bot.RenderMode) { - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard()) - return - } - - s.renderDetails(b, mode, jwt) + s.renderDetails(b, mode) } -func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt string) { - placement, err := b.Backend.GetPlacement( - context.Background(), - jwt, - s.WorkspaceID, - s.ProjectID, - s.PlacementID, - ) +func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) { + placement, err := b.Backend.GetPlacement(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, s.PlacementID) if err != nil { log.Error().Err(err).Msg("Failed to get placement") b.SendNew("❌ Не удалось загрузить размещение", Keyboard()) diff --git a/tg_bot/screens/project_details.go b/tg_bot/screens/project_details.go index 0c24753..954757d 100644 --- a/tg_bot/screens/project_details.go +++ b/tg_bot/screens/project_details.go @@ -13,7 +13,6 @@ import ( type ProjectDetails struct { Project *backend.Project - WorkspaceID string BackState bot.State confirmingLinkTypeChange bool // Флаг для двухшагового подтверждения смены типа ссылки } @@ -139,14 +138,12 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { ProjectTelegramID: s.Project.TelegramID, ProjectUsername: usernameFromProject(s.Project), ProjectStatus: statusFromProject(s.Project), - WorkspaceID: s.WorkspaceID, BackState: s, }, bot.EditMessage) case strings.HasPrefix(data, "placements:"): s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения b.SetState(&Placements{ - WorkspaceID: s.WorkspaceID, ProjectID: s.Project.ID, ProjectTitle: s.Project.Title, ProjectTelegramID: s.Project.TelegramID, @@ -173,20 +170,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { newType = "public" } - log.Info(). - Str("workspace_id", s.WorkspaceID). - Str("project_id", s.Project.ID). - Str("old_type", s.Project.PurchaseInviteTypeDefault). - Str("new_type", newType). - Msg("Updating project invite link type") - - updatedProject, err := b.Backend.UpdateProjectInviteLinkType( - context.Background(), - b.Session.JWT, - s.WorkspaceID, - s.Project.ID, - newType, - ) + updatedProject, err := b.Backend.UpdateProjectInviteLinkType(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.Project.ID, newType) if err != nil { b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), Keyboard( diff --git a/tg_bot/screens/purchase_optional_details.go b/tg_bot/screens/purchase_optional_details.go index 4d624b0..5e2a3f8 100644 --- a/tg_bot/screens/purchase_optional_details.go +++ b/tg_bot/screens/purchase_optional_details.go @@ -16,7 +16,6 @@ import ( ) type PurchaseOptionalDetails struct { - WorkspaceID string ProjectID string ProjectTitle string CreativeID string @@ -212,7 +211,6 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update) case "back": b.SetState(&SelectChannelsForPurchase{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, ProjectTitle: s.ProjectTitle, CreativeID: s.CreativeID, @@ -1432,13 +1430,7 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { Details: placementDetails, } - placements, err := b.Backend.CreatePlacements( - context.Background(), - jwt, - s.WorkspaceID, - s.ProjectID, - input, - ) + placements, err := b.Backend.CreatePlacements(context.Background(), jwt, b.Session.WorkspaceID, s.ProjectID, input) if err != nil { log.Error().Err(err).Msg("Failed to create placements") b.SendNew("❌ Не удалось создать размещения", Keyboard( @@ -1447,12 +1439,7 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { return } - creative, err := b.Backend.GetCreative( - context.Background(), - jwt, - s.WorkspaceID, - s.CreativeID, - ) + creative, err := b.Backend.GetCreative(context.Background(), jwt, b.Session.WorkspaceID, s.CreativeID) if err != nil { log.Error().Err(err).Msg("Failed to get creative") creative = nil @@ -1469,7 +1456,6 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { } b.SetState(&Placements{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, ProjectTitle: s.ProjectTitle, BackState: s.BackState, diff --git a/tg_bot/screens/purchases.go b/tg_bot/screens/purchases.go index d309377..a8cf72d 100644 --- a/tg_bot/screens/purchases.go +++ b/tg_bot/screens/purchases.go @@ -10,7 +10,6 @@ import ( ) type Placements struct { - WorkspaceID string ProjectID string ProjectTitle string ProjectTelegramID int64 @@ -20,23 +19,11 @@ type Placements struct { } func (s *Placements) Enter(b *bot.Bot, mode bot.RenderMode) { - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard()) - return - } - - s.renderPurchases(b, mode, jwt) + s.renderPurchases(b, mode) } -func (s *Placements) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string) { - resp, err := b.Backend.GetPlacements( - context.Background(), - jwt, - s.WorkspaceID, - s.ProjectID, - ) +func (s *Placements) renderPurchases(b *bot.Bot, mode bot.RenderMode) { + resp, err := b.Backend.GetPlacements(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID) if err != nil { log.Error().Err(err).Msg("Failed to get placements") b.SendNew("❌ Не удалось загрузить размещения", Keyboard()) @@ -114,7 +101,6 @@ func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) { case "add_purchase": b.SetState(&AddPurchase{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, ProjectTitle: s.ProjectTitle, ProjectTelegramID: s.ProjectTelegramID, @@ -129,7 +115,6 @@ func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) { if len(data) > 10 && data[:10] == "placement:" { placementID := data[10:] b.SetState(&PlacementDetails{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, PlacementID: placementID, BackState: s, diff --git a/tg_bot/screens/select_channels_for_purchase.go b/tg_bot/screens/select_channels_for_purchase.go index 38758f4..c38db33 100644 --- a/tg_bot/screens/select_channels_for_purchase.go +++ b/tg_bot/screens/select_channels_for_purchase.go @@ -20,7 +20,6 @@ type PurchaseChannelInput struct { } type SelectChannelsForPurchase struct { - WorkspaceID string ProjectID string ProjectTitle string ProjectTelegramID int64 @@ -143,7 +142,6 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat case "back": // Возвращаемся к выбору креатива b.SetState(&AddPurchase{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, ProjectTitle: s.ProjectTitle, ProjectTelegramID: s.ProjectTelegramID, @@ -186,7 +184,6 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat } b.SetState(&PurchaseOptionalDetails{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, ProjectTitle: s.ProjectTitle, CreativeID: s.CreativeID, @@ -256,13 +253,7 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) { Channels: apiChannels, } - placements, err := b.Backend.CreatePlacements( - context.Background(), - jwt, - s.WorkspaceID, - s.ProjectID, - input, - ) + placements, err := b.Backend.CreatePlacements(context.Background(), jwt, b.Session.WorkspaceID, s.ProjectID, input) if err != nil { log.Error().Err(err).Msg("Failed to create placements") @@ -280,7 +271,6 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) { } b.SetState(&PlacementDetails{ - WorkspaceID: s.WorkspaceID, ProjectID: s.ProjectID, PlacementID: placements.Placements[0].ID, BackState: s.BackState, diff --git a/tg_bot/screens/workspace_menu.go b/tg_bot/screens/workspace_menu.go new file mode 100644 index 0000000..7d795d8 --- /dev/null +++ b/tg_bot/screens/workspace_menu.go @@ -0,0 +1,134 @@ +package screens + +import ( + "context" + "fmt" + "strings" + + "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" +) + +type WorkspaceMenu struct { + CurrentPage int + BackState bot.State +} + +const msgWorkspaceEmpty = ` +У вас пока нет рабочих пространств. +` + +const workspacesPerPage = 6 + +func (s *WorkspaceMenu) Enter(b *bot.Bot, mode bot.RenderMode) { + workspaces, err := b.Backend.GetWorkspaces(context.Background(), b.Session.JWT) + if err != nil { + log.Error().Err(err).Msg("Failed to get workspaces for workspace menu") + b.SendNew("❌ Не удалось загрузить рабочие пространства", Keyboard(Row(Button("← Назад", "back")))) + return + } + + if len(workspaces) == 0 { + kb := Keyboard(Row(Button("← Назад", "back"))) + b.Render(msgWorkspaceEmpty, kb, mode) + return + } + + var allButtons []echotron.InlineKeyboardButton + for _, ws := range workspaces { + label := ws.Name + if ws.ID == b.Session.WorkspaceID { + label = "● " + label + } + allButtons = append(allButtons, Button(label, fmt.Sprintf("workspace_select:%s", ws.ID))) + } + + elementRows := ui.BuildElementRows(ui.ElementLayoutConfig{ + CurrentPage: s.CurrentPage, + ItemsPerPage: workspacesPerPage, + ItemsPerRow: 2, + }, allButtons) + + var buttons [][]echotron.InlineKeyboardButton + buttons = append(buttons, elementRows...) + + if navRow := ui.BuildNavigationRow(ui.PaginationConfig{ + CurrentPage: s.CurrentPage, + TotalItems: len(allButtons), + ItemsPerPage: workspacesPerPage, + }); navRow != nil { + buttons = append(buttons, navRow) + } + + buttons = append(buttons, Row(Button("← Назад", "back"))) + + totalPages := ui.CalculatePages(len(allButtons), workspacesPerPage) + text := fmt.Sprintf(`Рабочие пространства%s + +Выберите текущее рабочее пространство.`, ui.FormatPageInfo(s.CurrentPage, totalPages)) + + b.Render(text, Keyboard(buttons...), mode) +} + +func (s *WorkspaceMenu) 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 == "back": + if s.BackState != nil { + b.SetState(s.BackState, bot.EditMessage) + return + } + b.SetState(&MainMenu{}, bot.EditMessage) + + case strings.HasPrefix(data, "workspace_select:"): + parts := strings.Split(data, ":") + if len(parts) != 2 { + return + } + workspaceID := parts[1] + workspaces, err := b.Backend.GetWorkspaces(context.Background(), b.Session.JWT) + if err != nil { + log.Error().Err(err).Msg("Failed to get workspaces for selection") + b.SendNew("❌ Не удалось загрузить рабочие пространства", Keyboard(Row(Button("← Назад", "back")))) + return + } + for _, ws := range workspaces { + if ws.ID == workspaceID { + b.Session.WorkspaceID = ws.ID + break + } + } + + if s.BackState != nil { + b.SetState(s.BackState, bot.EditMessage) + return + } + b.SetState(&MainMenu{}, bot.EditMessage) + + default: + s.Enter(b, bot.NewMessage) + } +} + +func (s *WorkspaceMenu) HandleMessage(_ *bot.Bot, _ *echotron.Update) {} + +func (s *WorkspaceMenu) Handle(_ *bot.Bot, _ *echotron.Update) {} + +func (s *WorkspaceMenu) Exit() {}