diff --git a/pkg/telegram/telegram.go b/pkg/telegram/telegram.go
index bd56e42..c2702a5 100644
--- a/pkg/telegram/telegram.go
+++ b/pkg/telegram/telegram.go
@@ -52,6 +52,20 @@ func New(cfg Config) (*Client, error) {
return fmt.Errorf("session not authorized")
}
+ // Получаем информацию о текущем пользователе
+ self, err := tg.Self(ctx)
+ if err != nil {
+ log.Warn().Err(err).Msg("Failed to get user info")
+ } else {
+ log.Info().
+ Int64("id", self.ID).
+ Str("username", "@"+self.Username).
+ Str("first_name", self.FirstName).
+ Str("last_name", self.LastName).
+ Str("phone", self.Phone).
+ Msg("Authorized as user")
+ }
+
close(ready)
<-ctx.Done()
diff --git a/tg_bot/screens/add_purchase.go b/tg_bot/screens/add_purchase.go
index b362907..f361ed1 100644
--- a/tg_bot/screens/add_purchase.go
+++ b/tg_bot/screens/add_purchase.go
@@ -146,6 +146,10 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) {
text += `Для создания закупа сначала нужно создать креатив.
Вернитесь назад и создайте креатив в разделе Креативы`
+
+ buttons = append(buttons, Row(
+ Button("🎨 Перейти к креативам", "go_to_creatives"),
+ ))
} else {
text += fmt.Sprintf("Креативы%s\n\n", ui.FormatPageInfo(s.CreativePage, page.Pages))
@@ -219,6 +223,17 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.ActivePicker = ""
s.Enter(b, bot.EditMessage)
+ case "go_to_creatives":
+ b.SetState(&Creatives{
+ CurrentPage: 0,
+ ProjectID: s.ProjectID,
+ ProjectTitle: s.ProjectTitle,
+ ProjectTelegramID: s.ProjectTelegramID,
+ ProjectUsername: s.ProjectUsername,
+ ProjectStatus: s.ProjectStatus,
+ BackState: s,
+ }, bot.EditMessage)
+
case "prev":
if s.ActivePicker == "project_list" {
if s.ProjectPage > 0 {
diff --git a/tg_bot/screens/purchases.go b/tg_bot/screens/placements.go
similarity index 60%
rename from tg_bot/screens/purchases.go
rename to tg_bot/screens/placements.go
index a8cf72d..fd8649d 100644
--- a/tg_bot/screens/purchases.go
+++ b/tg_bot/screens/placements.go
@@ -6,7 +6,6 @@ import (
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
- "github.com/rs/zerolog/log"
)
type Placements struct {
@@ -18,72 +17,24 @@ type Placements struct {
BackState bot.State
}
+const msgPlacements = `
+Размещения
+
+Управление размещениями для проекта.
+`
+
func (s *Placements) Enter(b *bot.Bot, mode bot.RenderMode) {
- s.renderPurchases(b, mode)
-}
+ var rows [][]echotron.InlineKeyboardButton
-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())
- return
- }
+ rows = append(rows, Row(Button("+ Создать размещение", "add_purchase")))
+ rows = append(rows, Row(Button("Размещения", "placements_list")))
+ rows = append(rows, Row(URLButton("План закупов", "https://example.com")))
+ rows = append(rows, Row(Button("← Назад", "back")))
- text := "Размещения\n\n"
+ keyboard := Keyboard(rows...)
+ b.Render(msgPlacements, keyboard, mode)
- var buttons [][]echotron.InlineKeyboardButton
-
- if len(resp.Placements) == 0 {
- text += `Список размещений пуст.
-
-Размещение — это план публикации рекламы в канале:
- ‣ Выбор креатива
- ‣ Канал размещения
- ‣ Стоимость и формат
- ‣ Статус выполнения
-
-Создайте первое размещение`
- } else {
- text += "Управление размещениями для проекта.\n\n"
- text += "Выберите размещение для деталей:\n\n"
-
- for i, placement := range resp.Placements {
- statusSymbol := "●"
- if placement.Status == "archived" {
- statusSymbol = "■"
- }
-
- channelName := "Без названия"
- if placement.Channel.Title != nil && *placement.Channel.Title != "" {
- channelName = *placement.Channel.Title
- } else if placement.Channel.Username != nil && *placement.Channel.Username != "" {
- channelName = "@" + *placement.Channel.Username
- }
-
- buttonText := fmt.Sprintf("%s #%d %s", statusSymbol, i+1, channelName)
-
- buttons = append(buttons, Row(
- Button(buttonText, fmt.Sprintf("placement:%s", placement.ID)),
- ))
- }
- }
-
- // Кнопка создания нового размещения
- buttons = append(buttons, Row(
- Button("+ Создать размещение", "add_purchase"),
- ))
-
- // Кнопка назад
- buttons = append(buttons, Row(
- Button("← Назад", "back"),
- ))
-
- keyboard := Keyboard(buttons...)
- b.Render(text, keyboard, mode)
-
- messageID := b.LastMessageID
- updateProjectHeaderMedia(b, messageID, text, keyboard, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus)
+ updateProjectHeaderMedia(b, b.LastMessageID, msgPlacements, keyboard, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus)
}
func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) {
@@ -110,6 +61,106 @@ func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) {
BackState: s,
}, bot.EditMessage)
+ case "placements_list":
+ b.SetState(&PlacementsList{
+ ProjectID: s.ProjectID,
+ ProjectTitle: s.ProjectTitle,
+ ProjectTelegramID: s.ProjectTelegramID,
+ ProjectUsername: s.ProjectUsername,
+ ProjectStatus: s.ProjectStatus,
+ BackState: s,
+ }, bot.EditMessage)
+
+ default:
+ s.Enter(b, bot.NewMessage)
+ }
+}
+
+func (s *Placements) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
+
+func (s *Placements) Handle(_ *bot.Bot, _ *echotron.Update) { return }
+
+func (s *Placements) Exit() {}
+
+// PlacementsList - экран со списком размещений
+type PlacementsList struct {
+ ProjectID string
+ ProjectTitle string
+ ProjectTelegramID int64
+ ProjectUsername string
+ ProjectStatus string
+ BackState bot.State
+}
+
+func (s *PlacementsList) Enter(b *bot.Bot, mode bot.RenderMode) {
+ s.render(b, mode)
+}
+
+func (s *PlacementsList) render(b *bot.Bot, mode bot.RenderMode) {
+ resp, err := b.Backend.GetPlacements(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID)
+ if err != nil {
+ b.SendNew("❌ Не удалось загрузить размещения", Keyboard())
+ return
+ }
+
+ text := "Список размещений\n\n"
+
+ var buttons [][]echotron.InlineKeyboardButton
+
+ if len(resp.Placements) == 0 {
+ text += `Список размещений пуст.
+
+Размещение — это план публикации рекламы в канале:
+ ‣ Выбор креатива
+ ‣ Канал размещения
+ ‣ Стоимость и формат
+ ‣ Статус выполнения
+
+Создайте первое размещение`
+ } else {
+ for i, placement := range resp.Placements {
+ statusSymbol := "●"
+ if placement.Status == "archived" {
+ statusSymbol = "■"
+ }
+
+ channelName := "Без названия"
+ if placement.Channel.Title != nil && *placement.Channel.Title != "" {
+ channelName = *placement.Channel.Title
+ } else if placement.Channel.Username != nil && *placement.Channel.Username != "" {
+ channelName = "@" + *placement.Channel.Username
+ }
+
+ buttonText := fmt.Sprintf("%s #%d %s", statusSymbol, i+1, channelName)
+
+ buttons = append(buttons, Row(
+ Button(buttonText, fmt.Sprintf("placement:%s", placement.ID)),
+ ))
+ }
+ }
+
+ // Кнопка назад
+ buttons = append(buttons, Row(
+ Button("← Назад", "back"),
+ ))
+
+ keyboard := Keyboard(buttons...)
+ b.Render(text, keyboard, mode)
+}
+
+func (s *PlacementsList) HandleCallback(b *bot.Bot, u *echotron.Update) {
+ if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
+ return
+ }
+
+ data := u.CallbackQuery.Data
+
+ switch data {
+ case "back":
+ if s.BackState != nil {
+ b.SetState(s.BackState, bot.EditMessage)
+ }
+
default:
// Проверяем, не нажали ли на конкретное размещение
if len(data) > 10 && data[:10] == "placement:" {
@@ -125,8 +176,8 @@ func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) {
}
}
-func (s *Placements) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
+func (s *PlacementsList) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
-func (s *Placements) Handle(_ *bot.Bot, _ *echotron.Update) { return }
+func (s *PlacementsList) Handle(_ *bot.Bot, _ *echotron.Update) { return }
-func (s *Placements) Exit() {}
+func (s *PlacementsList) Exit() {}
diff --git a/tg_bot/screens/project_details.go b/tg_bot/screens/project_details.go
index 954757d..54f902e 100644
--- a/tg_bot/screens/project_details.go
+++ b/tg_bot/screens/project_details.go
@@ -12,15 +12,14 @@ import (
)
type ProjectDetails struct {
- Project *backend.Project
- BackState bot.State
- confirmingLinkTypeChange bool // Флаг для двухшагового подтверждения смены типа ссылки
+ Project *backend.Project
+ BackState bot.State
}
func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
if s.Project == nil {
b.SendNew("❌ Проект не найден", Keyboard(
- Row(Button("← Назад", "back_to_projects")),
+ Row(Button("← Назад", "back")),
))
return
}
@@ -40,32 +39,25 @@ func (s *ProjectDetails) formatProjectDetails() string {
text := fmt.Sprintf("%s\n\n", p.Title)
- // Информация о канале
if p.Username != nil && *p.Username != "" {
- text += fmt.Sprintf("▸ Telegram: @%s\n", *p.Username)
+ text += fmt.Sprintf("▸ Tg username: @%s\n", *p.Username)
} else {
- text += "▸ Telegram: не привязан\n"
+ text += "▸ Tg username: не привязан\n"
}
// Статус проекта
- var statusSymbol string
- var statusText string
+ var statusSymbol, statusText string
switch p.Status {
case "active":
- statusSymbol = "●"
- statusText = "Активный"
+ statusSymbol, statusText = "●", "Активный"
case "inactive":
- statusSymbol = "○"
- statusText = "Неактивен"
+ statusSymbol, statusText = "○", "Неактивен"
case "archived":
- statusSymbol = "■"
- statusText = "Архивный"
+ statusSymbol, statusText = "■", "Архивный"
case "paused":
- statusSymbol = "◐"
- statusText = "Приостановлен"
+ statusSymbol, statusText = "◐", "Приостановлен"
default:
- statusSymbol = "○"
- statusText = p.Status
+ statusSymbol, statusText = "○", p.Status
}
text += fmt.Sprintf("▸ Статус: %s %s\n", statusSymbol, statusText)
@@ -82,34 +74,20 @@ func (s *ProjectDetails) buildKeyboard() echotron.InlineKeyboardMarkup {
Button("Размещения", fmt.Sprintf("placements:%s", s.Project.ID)),
))
- // Второй ряд: Тип вступления по умолчанию для закупов (с подтверждением)
- var linkTypeButtonText string
- if s.confirmingLinkTypeChange {
- // Показываем подтверждение
- var newType string
- if s.Project.PurchaseInviteTypeDefault == "public" {
- newType = "По заявке"
- } else {
- newType = "Свободное"
- }
- linkTypeButtonText = fmt.Sprintf("Изменить → %s", newType)
+ // Второй ряд: Тип вступления по умолчанию для закупов
+ var currentType string
+ if s.Project.PurchaseInviteTypeDefault == "public" {
+ currentType = "открытая"
} else {
- // Показываем текущий тип вступления
- var currentType string
- if s.Project.PurchaseInviteTypeDefault == "public" {
- currentType = "Свободное"
- } else {
- currentType = "По заявке"
- }
- linkTypeButtonText = fmt.Sprintf("Вступление: %s", currentType)
+ currentType = "с заявками"
}
buttons = append(buttons, Row(
- Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)),
+ Button(fmt.Sprintf("Ссылка: %s", currentType), fmt.Sprintf("link_type:%s", s.Project.ID)),
))
// Нижний ряд: Назад и Архивировать
buttons = append(buttons, Row(
- Button("← Назад", "back_to_projects"),
+ Button("← Назад", "back"),
Button("≡ Архивировать", fmt.Sprintf("archive:%s", s.Project.ID)),
))
@@ -124,14 +102,12 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
data := u.CallbackQuery.Data
switch {
- case data == "back_to_projects", data == "back":
- s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
+ case data == "back":
if s.BackState != nil {
s.transitionWithNewMessage(b, s.BackState)
}
case strings.HasPrefix(data, "creatives:"):
- s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
b.SetState(&Creatives{
ProjectID: s.Project.ID,
ProjectTitle: s.Project.Title,
@@ -142,7 +118,6 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
}, bot.EditMessage)
case strings.HasPrefix(data, "placements:"):
- s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
b.SetState(&Placements{
ProjectID: s.Project.ID,
ProjectTitle: s.Project.Title,
@@ -153,38 +128,28 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
}, bot.EditMessage)
case data == "back_to_project":
- s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
s.Enter(b, bot.EditMessage)
case strings.HasPrefix(data, "link_type:"):
- if !s.confirmingLinkTypeChange {
- // Первое нажатие - включаем режим подтверждения
- s.confirmingLinkTypeChange = true
- s.Enter(b, bot.EditMessage)
+ var newType string
+ if s.Project.PurchaseInviteTypeDefault == "public" {
+ newType = "approval"
} else {
- // Второе нажатие - меняем тип ссылки
- var newType string
- if s.Project.PurchaseInviteTypeDefault == "public" {
- newType = "approval"
- } else {
- newType = "public"
- }
-
- 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(
- Row(Button("← Назад", "back_to_project")),
- ))
- return
- }
-
- // Обновляем проект и сбрасываем флаг подтверждения
- s.Project = updatedProject
- s.confirmingLinkTypeChange = false
- s.Enter(b, bot.EditMessage)
+ newType = "public"
}
+ 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(
+ Row(Button("← Назад", "back_to_project")),
+ ))
+ return
+ }
+
+ s.Project = updatedProject
+ s.Enter(b, bot.EditMessage)
+
default:
// Для остальных кнопок показываем заглушку
b.Edit(fmt.Sprintf("🚧 Функция в разработке\n\nCallback: %s", data), Keyboard(