diff --git a/telegram_bot/bot/bot.go b/telegram_bot/bot/bot.go
index 1f00c88..175695c 100644
--- a/telegram_bot/bot/bot.go
+++ b/telegram_bot/bot/bot.go
@@ -12,33 +12,6 @@ import (
"github.com/rs/zerolog/log"
)
-func Button(text, data string) echotron.InlineKeyboardButton {
- return echotron.InlineKeyboardButton{
- Text: text,
- CallbackData: data,
- }
-}
-
-func URLButton(text, url string) echotron.InlineKeyboardButton {
- return echotron.InlineKeyboardButton{
- Text: text,
- URL: url,
- }
-}
-
-func Row(buttons ...echotron.InlineKeyboardButton) []echotron.InlineKeyboardButton { return buttons }
-
-func Keyboard(rows ...[]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
- if len(rows) == 0 {
- return echotron.InlineKeyboardMarkup{
- InlineKeyboard: [][]echotron.InlineKeyboardButton{},
- }
- }
- return echotron.InlineKeyboardMarkup{
- InlineKeyboard: rows,
- }
-}
-
type Exec struct {
handled bool
transitioned bool
@@ -363,7 +336,10 @@ func (b *Bot) Update(u *echotron.Update) {
}
func (b *Bot) cleanupKeyboard(messageID int) {
- emptyKeyboard := Keyboard()
+ emptyKeyboard := echotron.InlineKeyboardMarkup{
+ InlineKeyboard: [][]echotron.InlineKeyboardButton{},
+ }
+
_, err := b.EditMessageReplyMarkup(
echotron.NewMessageID(b.ChatID, messageID),
&echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard},
diff --git a/telegram_bot/screens/accept_workspace_invite.go b/telegram_bot/screens/accept_workspace_invite.go
index d2a35f4..aa5f778 100644
--- a/telegram_bot/screens/accept_workspace_invite.go
+++ b/telegram_bot/screens/accept_workspace_invite.go
@@ -5,7 +5,6 @@ import (
"example.com/m/bot"
"github.com/NicoNex/echotron/v3"
- "github.com/rs/zerolog/log"
)
type AcceptWorkspaceInvite struct {
@@ -18,38 +17,29 @@ const msgAcceptWorkspaceInviteSuccessfully = `
`
func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) {
- // Получаем JWT токен
- jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
- if err != nil {
- log.Error().Err(err).Msg("Failed to get JWT")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
+ jwt := b.Session.JWT
+ if jwt == "" {
+ b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", emptyKeyboard)
return
}
- // Принимаем приглашение
- err = b.Backend.AcceptWorkspaceInvite(context.Background(), jwt, s.InviteID)
+ err := b.Backend.AcceptWorkspaceInvite(context.Background(), jwt, s.InviteID)
if err != nil {
- log.Error().Err(err).Str("invite_id", s.InviteID).Msg("Failed to accept workspace invite")
- b.SendNew("❌ Не удалось принять приглашение. Возможно, оно уже было использовано.", bot.Keyboard())
+ b.SendNew("❌ Не удалось принять приглашение. Возможно, оно уже было использовано.", emptyKeyboard)
return
}
- b.SendNew(msgAcceptWorkspaceInviteSuccessfully, bot.Keyboard(
- bot.Row(bot.Button("🏠 Главное меню", "menu")),
- ))
+ kb := Keyboard(
+ Row(Button("Главное меню", "menu")),
+ )
+
+ b.SendNew(msgAcceptWorkspaceInviteSuccessfully, kb)
}
-func (s *AcceptWorkspaceInvite) HandleCallback(b *bot.Bot, u *echotron.Update) {
- if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
- return
- }
+func (s *AcceptWorkspaceInvite) HandleCallback(b *bot.Bot, u *echotron.Update) {}
- if u.CallbackQuery.Data == "menu" {
- b.SetState(&MainMenu{}, bot.EditMessage)
- }
- return
+func (s *AcceptWorkspaceInvite) HandleMessage(_ *bot.Bot, _ *echotron.Update) {}
+
+func (s *AcceptWorkspaceInvite) Handle(b *bot.Bot, _ *echotron.Update) {
+ b.SetState(&MainMenu{}, bot.EditMessage)
}
-
-func (s *AcceptWorkspaceInvite) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
-
-func (s *AcceptWorkspaceInvite) Handle(_ *bot.Bot, _ *echotron.Update) { return }
diff --git a/telegram_bot/screens/add_creative.go b/telegram_bot/screens/add_creative.go
index 9718660..bba187a 100644
--- a/telegram_bot/screens/add_creative.go
+++ b/telegram_bot/screens/add_creative.go
@@ -23,8 +23,8 @@ type AddCreative struct {
WaitingForTextEdit bool // Ждем редактирования текста креатива
}
-func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
- text := `… › Креативы › Добавление
+const msgWaitCreative = `
+… › Креативы › Добавление
+ Добавить креатив
@@ -32,15 +32,13 @@ func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
• Обрабатываем любое сообщение Telegram
• Отправляйте сразу оформленный пост
-• На следующем шаге можно добавить кнопки или изменить контент`
+• На следующем шаге можно добавить кнопки или изменить контент
+`
- buttons := [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel")),
- }
-
- keyboard := bot.Keyboard(buttons...)
+func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
+ kb := Keyboard(Row(Button("« Отмена", "cancel")))
s.WaitingForCreative = true
- b.Render(text, keyboard, mode)
+ b.Render(msgWaitCreative, kb, mode)
}
func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
@@ -69,7 +67,7 @@ func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
Пример:
Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_edit")),
+ Row(Button("« Отмена", "cancel_edit")),
})
case data == "add_button":
@@ -86,11 +84,11 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
• Написать нам
Ссылка на канал подставится автоматически при создании закупа.`, [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button("✓ Ссылка на канал", "button_type_invite"),
- bot.Button("Своя ссылка", "button_type_custom"),
+ Row(
+ Button("✓ Ссылка на канал", "button_type_invite"),
+ Button("Своя ссылка", "button_type_custom"),
),
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
case data == "add_media":
@@ -102,7 +100,7 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
Отправьте фото, видео или GIF
Это медиа будет отображаться в креативе вместе с текстом`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_media")),
+ Row(Button("« Отмена", "cancel_media")),
})
case data == "delete_media":
@@ -150,11 +148,11 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
• Написать нам
Ссылка на канал подставится автоматически при создании закупа.`, [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button("✓ Ссылка на канал", "button_type_invite"),
- bot.Button("Своя ссылка", "button_type_custom"),
+ Row(
+ Button("✓ Ссылка на канал", "button_type_invite"),
+ Button("Своя ссылка", "button_type_custom"),
),
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
case data == "button_type_custom":
@@ -168,11 +166,11 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
• Написать нам
Ссылка на канал подставится автоматически при создании закупа.`, [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button("Ссылка на канал", "button_type_invite"),
- bot.Button("✓ Своя ссылка", "button_type_custom"),
+ Row(
+ Button("Ссылка на канал", "button_type_invite"),
+ Button("✓ Своя ссылка", "button_type_custom"),
),
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
case data == "cancel_edit":
@@ -321,7 +319,7 @@ URL должен начинаться с https://
Например:
https://example.com
https://t.me/yourchannel`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
} else {
s.Buttons = append(s.Buttons, InlineButton{
@@ -359,7 +357,7 @@ URL должен начинаться с https://
URL должен начинаться с https://
Попробуйте ещё раз.`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
return
}
@@ -472,23 +470,20 @@ func (s *AddCreative) extractCreativeFromMessage(message *echotron.Message) {
// generateCreativeName генерирует название из текста или дефолтное
func (s *AddCreative) generateCreativeName() string {
- // Если есть текст, берем первые слова
if s.Text != nil && *s.Text != "" {
// Удаляем ВСЕ HTML теги с помощью regex
re := regexp.MustCompile(`<[^>]*>`)
cleanText := re.ReplaceAllString(*s.Text, "")
- // Берем первые 50 символов
runes := []rune(strings.TrimSpace(cleanText))
- if len(runes) > 50 {
- return string(runes[:50]) + "..."
+ if len(runes) > 10 {
+ return string(runes[:10]) + "..."
}
if len(runes) > 0 {
return string(runes)
}
}
- // Дефолтное название
return "Новый креатив"
}
@@ -505,16 +500,16 @@ func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][
var buttons [][]echotron.InlineKeyboardButton
// Кнопки редактирования
- buttons = append(buttons, bot.Row(
- bot.Button("✎ Текст", "edit_text"),
- bot.Button("+ Медиа", "add_media"),
+ buttons = append(buttons, Row(
+ Button("✎ Текст", "edit_text"),
+ Button("+ Медиа", "add_media"),
))
- buttons = append(buttons, bot.Row(bot.Button("+ Добавить кнопку", "add_button")))
+ buttons = append(buttons, Row(Button("+ Добавить кнопку", "add_button")))
// Кнопка удаления медиа (если медиа добавлено)
if s.MediaFileID != "" {
- buttons = append(buttons, bot.Row(
- bot.Button("⌫ Убрать медиа", "delete_media"),
+ buttons = append(buttons, Row(
+ Button("⌫ Убрать медиа", "delete_media"),
))
}
@@ -522,7 +517,7 @@ func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][
if len(s.Buttons) > 0 {
var row []echotron.InlineKeyboardButton
for i, btn := range s.Buttons {
- row = append(row, bot.Button(
+ row = append(row, Button(
fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text),
fmt.Sprintf("delete_button:%d", i),
))
@@ -540,13 +535,13 @@ func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][
// Финальные кнопки
if s.Text != nil {
- buttons = append(buttons, bot.Row(
- bot.Button("← "+cancelCallback, "cancel"),
- bot.Button(confirmText, "confirm_create"),
+ buttons = append(buttons, Row(
+ Button("← "+cancelCallback, "cancel"),
+ Button(confirmText, "confirm_create"),
))
} else {
- buttons = append(buttons, bot.Row(
- bot.Button("← "+cancelCallback, "cancel"),
+ buttons = append(buttons, Row(
+ Button("← "+cancelCallback, "cancel"),
))
}
@@ -592,7 +587,7 @@ func (s *AddCreative) createCreative(b *bot.Bot) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start login", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start login", Keyboard())
return
}
@@ -692,7 +687,7 @@ https://t.me/+AbCdEfGhIjKlMn
🔘 Кнопок добавлено: %d`, len(s.Buttons))
}
- b.SendNew(successText, bot.Keyboard())
+ b.SendNew(successText, Keyboard())
// Возвращаемся на экран креативов
if s.BackState != nil {
diff --git a/telegram_bot/screens/add_project.go b/telegram_bot/screens/add_project.go
index f2cb5c2..bb46f1e 100644
--- a/telegram_bot/screens/add_project.go
+++ b/telegram_bot/screens/add_project.go
@@ -30,10 +30,10 @@ type AddProject struct {
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
buttons := [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("← Назад", "back")),
+ Row(Button("← Назад", "back")),
}
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(addProjectInstructionText, keyboard, mode)
}
diff --git a/telegram_bot/screens/add_purchase.go b/telegram_bot/screens/add_purchase.go
index 4b4ad97..8375af1 100644
--- a/telegram_bot/screens/add_purchase.go
+++ b/telegram_bot/screens/add_purchase.go
@@ -32,7 +32,7 @@ func (s *AddPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return
}
@@ -55,11 +55,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
}
if s.ActivePicker == "" {
- buttons = append(buttons, bot.Row(
- bot.Button(projectLabel, "pick_project"),
+ buttons = append(buttons, Row(
+ Button(projectLabel, "pick_project"),
))
- buttons = append(buttons, bot.Row(
- bot.Button(creativeLabel, "pick_creative"),
+ buttons = append(buttons, Row(
+ Button(creativeLabel, "pick_creative"),
))
}
@@ -74,7 +74,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
)
if err != nil {
log.Error().Err(err).Msg("Failed to get projects")
- b.SendNew("❌ Не удалось загрузить проекты", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить проекты", Keyboard())
return
}
@@ -90,11 +90,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
var slots []echotron.InlineKeyboardButton
for _, project := range page.Items {
- slots = append(slots, bot.Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
+ slots = append(slots, Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
}
if page.Pages >= 2 {
for len(slots) < addPurchaseProjectsPerPage {
- slots = append(slots, bot.Button(" ", "empty"))
+ slots = append(slots, Button(" ", "empty"))
}
}
for i := 0; i < len(slots); i += 2 {
@@ -102,7 +102,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
buttons = append(buttons, row)
}
@@ -115,15 +115,15 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
}
}
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back_to_selection"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back_to_selection"),
))
case "creative_list":
if s.ProjectID == "" {
text += "Сначала выберите проект, чтобы увидеть список креативов.\n\n"
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back_to_selection"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back_to_selection"),
))
break
}
@@ -139,7 +139,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
)
if err != nil {
log.Error().Err(err).Msg("Failed to get creatives")
- b.SendNew("❌ Не удалось загрузить креативы", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить креативы", Keyboard())
return
}
@@ -157,11 +157,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
var slots []echotron.InlineKeyboardButton
for _, creative := range page.Items {
- slots = append(slots, bot.Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
+ slots = append(slots, Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
}
if page.Pages >= 2 {
for len(slots) < addPurchaseCreativesPerPage {
- slots = append(slots, bot.Button(" ", "empty"))
+ slots = append(slots, Button(" ", "empty"))
}
}
for i := 0; i < len(slots); i += 2 {
@@ -169,7 +169,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
buttons = append(buttons, row)
}
@@ -182,25 +182,25 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
}
}
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back_to_selection"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back_to_selection"),
))
}
if s.ActivePicker == "" {
if s.ProjectID != "" && s.CreativeID != "" {
- buttons = append(buttons, bot.Row(
- bot.Button("← Отмена", "back"),
- bot.Button("→ Далее", "next"),
+ buttons = append(buttons, Row(
+ Button("← Отмена", "back"),
+ Button("→ Далее", "next"),
))
} else {
- buttons = append(buttons, bot.Row(
- bot.Button("← Отмена", "back"),
+ buttons = append(buttons, Row(
+ Button("← Отмена", "back"),
))
}
}
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
diff --git a/telegram_bot/screens/creative_details.go b/telegram_bot/screens/creative_details.go
index 958728c..93d7d87 100644
--- a/telegram_bot/screens/creative_details.go
+++ b/telegram_bot/screens/creative_details.go
@@ -24,11 +24,10 @@ type CreativeDetails struct {
}
func (s *CreativeDetails) 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("❌ Ошибка авторизации\n\nПопробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", Keyboard())
return
}
@@ -36,8 +35,8 @@ func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
creative, err := b.Backend.GetCreative(context.Background(), jwt, s.WorkspaceID, s.CreativeID)
if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative")
- b.SendNew("❌ Не удалось загрузить креатив", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back")),
+ b.SendNew("❌ Не удалось загрузить креатив", Keyboard(
+ Row(Button("← Назад", "back")),
))
return
}
@@ -96,7 +95,7 @@ func (s *CreativeDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
Пример:
Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена редактирования", "cancel_edit")),
+ Row(Button("« Отмена редактирования", "cancel_edit")),
})
case data == "add_button":
@@ -111,11 +110,11 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
• Написать нам
Ссылка на канал подставится автоматически при создании закупа.`, [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button("✓ Ссылка на канал", "button_type_invite"),
- bot.Button("Своя ссылка", "button_type_custom"),
+ Row(
+ Button("✓ Ссылка на канал", "button_type_invite"),
+ Button("Своя ссылка", "button_type_custom"),
),
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
case data == "add_media":
@@ -125,7 +124,7 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
Отправьте фото, видео или GIF
Это медиа будет отображаться в креативе вместе с текстом`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_media")),
+ Row(Button("« Отмена", "cancel_media")),
})
case data == "delete_media":
@@ -161,11 +160,11 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
• Написать нам
Ссылка на канал подставится автоматически при создании закупа.`, [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button("✓ Ссылка на канал", "button_type_invite"),
- bot.Button("Своя ссылка", "button_type_custom"),
+ Row(
+ Button("✓ Ссылка на канал", "button_type_invite"),
+ Button("Своя ссылка", "button_type_custom"),
),
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
case data == "button_type_custom":
@@ -179,11 +178,11 @@ https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
• Написать нам
Ссылка на канал подставится автоматически при создании закупа.`, [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button("Ссылка на канал", "button_type_invite"),
- bot.Button("✓ Своя ссылка", "button_type_custom"),
+ Row(
+ Button("Ссылка на канал", "button_type_invite"),
+ Button("✓ Своя ссылка", "button_type_custom"),
),
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
case data == "cancel_edit", data == "cancel_media":
@@ -298,7 +297,7 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
Например:
https://example.com`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
} else {
s.Buttons = append(s.Buttons, InlineButton{
@@ -329,7 +328,7 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
URL должен начинаться с http:// или https://
Попробуйте ещё раз:`, [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("« Отмена", "cancel_add_button")),
+ Row(Button("« Отмена", "cancel_add_button")),
})
return
}
@@ -394,7 +393,7 @@ func (s *CreativeDetails) showManagementPanelWithDelete(b *bot.Bot) {
}
// Добавляем кнопку удаления креатива
- deleteButton := bot.Row(bot.Button("⌦ Удалить креатив", "delete_creative"))
+ deleteButton := Row(Button("⌦ Удалить креатив", "delete_creative"))
s.ShowManagementPanel(b, confirmText, confirmCallback, "cancel", deleteButton)
}
@@ -404,7 +403,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", Keyboard())
return
}
@@ -427,8 +426,8 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
log.Error().Err(err).Msg("Failed to download creative media")
// Показываем ошибку через панель управления
s.ShowControlPanel(b, "❌ Не удалось загрузить медиа\n\nПопробуйте удалить медиа и добавить заново, или нажмите \"Сохранить\" без изменения медиа.", [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("← Назад", "back")),
- bot.Row(bot.Button("✓ Сохранить без медиа", "confirm_save")),
+ Row(Button("← Назад", "back")),
+ Row(Button("✓ Сохранить без медиа", "confirm_save")),
})
return
}
@@ -461,8 +460,8 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to update creative")
// Показываем ошибку через панель управления
s.ShowControlPanel(b, "❌ Не удалось сохранить изменения\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("← Назад", "back")),
- bot.Row(bot.Button("✓ Попробовать снова", "confirm_save")),
+ Row(Button("← Назад", "back")),
+ Row(Button("✓ Попробовать снова", "confirm_save")),
})
return
}
@@ -471,7 +470,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
// Возвращаемся назад
if s.BackState != nil {
- b.SendNew("✅ Креатив успешно обновлен!", bot.Keyboard())
+ b.SendNew("✅ Креатив успешно обновлен!", Keyboard())
b.SetState(s.BackState, bot.EditMessage)
}
}
@@ -481,7 +480,7 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", Keyboard())
return
}
@@ -498,8 +497,8 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
// Показываем ошибку через панель управления
s.confirmingDelete = false // Сбрасываем флаг подтверждения
s.ShowControlPanel(b, "❌ Не удалось удалить креатив\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
- bot.Row(bot.Button("← Назад", "back")),
- bot.Row(bot.Button("⌦ Попробовать снова", "delete_creative")),
+ Row(Button("← Назад", "back")),
+ Row(Button("⌦ Попробовать снова", "delete_creative")),
})
return
}
@@ -508,7 +507,7 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
// Возвращаемся назад
if s.BackState != nil {
- b.SendNew("✅ Креатив успешно удален!", bot.Keyboard())
+ b.SendNew("✅ Креатив успешно удален!", Keyboard())
b.SetState(s.BackState, bot.EditMessage)
}
}
diff --git a/telegram_bot/screens/creative_editor.go b/telegram_bot/screens/creative_editor.go
index 8f51d15..4273b08 100644
--- a/telegram_bot/screens/creative_editor.go
+++ b/telegram_bot/screens/creative_editor.go
@@ -365,17 +365,17 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
var buttons [][]echotron.InlineKeyboardButton
// Блок редактирования контента
- buttons = append(buttons, bot.Row(
- bot.Button("✎ Текст", "edit_text"),
- bot.Button("+ Медиа", "add_media"),
+ buttons = append(buttons, Row(
+ Button("✎ Текст", "edit_text"),
+ Button("+ Медиа", "add_media"),
))
// Добавление кнопки сразу под основным блоком
- buttons = append(buttons, bot.Row(bot.Button("+ Добавить кнопку", "add_button")))
+ buttons = append(buttons, Row(Button("+ Добавить кнопку", "add_button")))
// Кнопка удаления медиа (если медиа добавлено)
if e.MediaFileID != "" {
- buttons = append(buttons, bot.Row(
- bot.Button("⌫ Убрать медиа", "delete_media"),
+ buttons = append(buttons, Row(
+ Button("⌫ Убрать медиа", "delete_media"),
))
}
@@ -383,7 +383,7 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
if len(e.Buttons) > 0 {
var row []echotron.InlineKeyboardButton
for i, btn := range e.Buttons {
- row = append(row, bot.Button(
+ row = append(row, Button(
fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text),
fmt.Sprintf("delete_button:%d", i),
))
@@ -406,13 +406,13 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
// Финальные кнопки
if e.Text != nil {
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", cancelCallback),
- bot.Button(confirmButtonText, confirmCallback),
+ buttons = append(buttons, Row(
+ Button("← Назад", cancelCallback),
+ Button(confirmButtonText, confirmCallback),
))
} else {
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", cancelCallback),
+ buttons = append(buttons, Row(
+ Button("← Назад", cancelCallback),
))
}
@@ -423,7 +423,7 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons [][]echotron.InlineKeyboardButton) {
if e.ControlPanelMessageID == nil {
// Отправляем новую панель
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.SendNew(text, keyboard)
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
controlPanelMsgID := b.LastMessageID
@@ -437,7 +437,7 @@ func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons
oldLastMessageID := b.LastMessageID
b.LastMessageID = *e.ControlPanelMessageID
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Edit(text, keyboard)
// Восстанавливаем оригинальный LastMessageID
diff --git a/telegram_bot/screens/creatives.go b/telegram_bot/screens/creatives.go
index 7188ef7..d56fd43 100644
--- a/telegram_bot/screens/creatives.go
+++ b/telegram_bot/screens/creatives.go
@@ -26,7 +26,7 @@ func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", Keyboard())
return
}
@@ -45,7 +45,7 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
)
if err != nil {
log.Error().Err(err).Msg("Failed to get creatives")
- b.SendNew("❌ Не удалось загрузить креативы", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить креативы", Keyboard())
return
}
@@ -87,13 +87,13 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
// Создаем слоты для креативов
var slots []echotron.InlineKeyboardButton
for _, creative := range page.Items {
- slots = append(slots, bot.Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
+ slots = append(slots, Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
}
// Если страниц >= 2, заполняем до 6 слотов пустыми кнопками
if page.Pages >= 2 {
for len(slots) < creativesPerPage {
- slots = append(slots, bot.Button(" ", "empty"))
+ slots = append(slots, Button(" ", "empty"))
}
}
@@ -107,7 +107,7 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
row = append(row, slots[i+1])
} else {
// Если нечетное количество, добавляем пустую кнопку
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
buttons = append(buttons, row)
@@ -117,18 +117,18 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
TotalPages: page.Pages,
- MiddleButtons: []echotron.InlineKeyboardButton{bot.Button("+ Добавить", "add_creative")},
+ MiddleButtons: []echotron.InlineKeyboardButton{Button("+ Добавить", "add_creative")},
}); navRow != nil {
buttons = append(buttons, navRow)
}
// Нижний ряд: Назад + Архив
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back"),
- bot.Button("≡ Архив", "archive"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back"),
+ Button("≡ Архив", "archive"),
))
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
@@ -157,8 +157,8 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
case data == "archive":
// TODO: реализовать экран архива креативов
- b.Edit("📦 Архив креативов\n\nЭта функция в разработке", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back")),
+ b.Edit("📦 Архив креативов\n\nЭта функция в разработке", Keyboard(
+ Row(Button("← Назад", "back")),
))
case data == "add_creative":
diff --git a/telegram_bot/screens/date_time_picker.go b/telegram_bot/screens/date_time_picker.go
index a4056e1..7cd515e 100644
--- a/telegram_bot/screens/date_time_picker.go
+++ b/telegram_bot/screens/date_time_picker.go
@@ -175,20 +175,20 @@ func (s *DateTimePicker) renderCalendarTitle() string {
func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
var rows [][]echotron.InlineKeyboardButton
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.monthPrevButton(),
- bot.Button(monthName(s.month), dtpCallbackPrefix+"open_months"),
- bot.Button("→", dtpCallbackPrefix+"month_next"),
+ Button(monthName(s.month), dtpCallbackPrefix+"open_months"),
+ Button("→", dtpCallbackPrefix+"month_next"),
))
- rows = append(rows, bot.Row(
- bot.Button("Пн", "empty"),
- bot.Button("Вт", "empty"),
- bot.Button("Ср", "empty"),
- bot.Button("Чт", "empty"),
- bot.Button("Пт", "empty"),
- bot.Button("Сб", "empty"),
- bot.Button("Вс", "empty"),
+ rows = append(rows, Row(
+ Button("Пн", "empty"),
+ Button("Вт", "empty"),
+ Button("Ср", "empty"),
+ Button("Чт", "empty"),
+ Button("Пт", "empty"),
+ Button("Сб", "empty"),
+ Button("Вс", "empty"),
))
firstOfMonth := time.Date(s.year, s.month, 1, 0, 0, 0, 0, mskLocation)
@@ -202,16 +202,16 @@ func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
var row []echotron.InlineKeyboardButton
for i := 1; i < weekday; i++ {
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
for day := 1; day <= daysInMonth; day++ {
date := time.Date(s.year, s.month, day, 0, 0, 0, 0, mskLocation)
if date.Before(todayDate) {
- row = append(row, bot.Button("-", "empty"))
+ row = append(row, Button("-", "empty"))
} else {
label := fmt.Sprintf("%d", day)
- row = append(row, bot.Button(label, fmt.Sprintf("%sday:%04d-%02d-%02d", dtpCallbackPrefix, s.year, int(s.month), day)))
+ row = append(row, Button(label, fmt.Sprintf("%sday:%04d-%02d-%02d", dtpCallbackPrefix, s.year, int(s.month), day)))
}
if len(row) == 7 {
rows = append(rows, row)
@@ -221,17 +221,17 @@ func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
if len(row) > 0 {
for len(row) < 7 {
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
rows = append(rows, row)
}
- rows = append(rows, bot.Row(
- bot.Button("← Назад", "back"),
- bot.Button("Отмена", "cancel"),
+ rows = append(rows, Row(
+ Button("← Назад", "back"),
+ Button("Отмена", "cancel"),
))
- return bot.Keyboard(rows...)
+ return Keyboard(rows...)
}
func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup {
@@ -239,25 +239,25 @@ func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup {
months := []string{"Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"}
for i := 0; i < 12; i += 3 {
- rows = append(rows, bot.Row(
- bot.Button(months[i], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+1)),
- bot.Button(months[i+1], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+2)),
- bot.Button(months[i+2], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+3)),
+ rows = append(rows, Row(
+ Button(months[i], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+1)),
+ Button(months[i+1], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+2)),
+ Button(months[i+2], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+3)),
))
}
- rows = append(rows, bot.Row(
- bot.Button("← Назад", "back"),
+ rows = append(rows, Row(
+ Button("← Назад", "back"),
))
- return bot.Keyboard(rows...)
+ return Keyboard(rows...)
}
func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup {
var rows [][]echotron.InlineKeyboardButton
- rows = append(rows, bot.Row(bot.Button("Часы", "empty")))
+ rows = append(rows, Row(Button("Часы", "empty")))
for i := 0; i < 24; i += 6 {
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.hourButton(i),
s.hourButton(i+1),
s.hourButton(i+2),
@@ -266,10 +266,10 @@ func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup {
s.hourButton(i+5),
))
}
- rows = append(rows, bot.Row(bot.Button("Минуты", "empty")))
+ rows = append(rows, Row(Button("Минуты", "empty")))
minutes := []int{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}
for i := 0; i < len(minutes); i += 6 {
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.minuteButton(minutes[i]),
s.minuteButton(minutes[i+1]),
s.minuteButton(minutes[i+2]),
@@ -278,18 +278,18 @@ func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup {
s.minuteButton(minutes[i+5]),
))
}
- rows = append(rows, bot.Row(
- bot.Button("← Назад", "back"),
- bot.Button("Готово", dtpCallbackPrefix+"confirm"),
+ rows = append(rows, Row(
+ Button("← Назад", "back"),
+ Button("Готово", dtpCallbackPrefix+"confirm"),
))
- return bot.Keyboard(rows...)
+ return Keyboard(rows...)
}
func (s *DateTimePicker) confirmKeyboard() echotron.InlineKeyboardMarkup {
- return bot.Keyboard(
- bot.Row(
- bot.Button("← Назад", "back"),
- bot.Button("Готово", dtpCallbackPrefix+"confirm"),
+ return Keyboard(
+ Row(
+ Button("← Назад", "back"),
+ Button("Готово", dtpCallbackPrefix+"confirm"),
),
)
}
@@ -426,23 +426,23 @@ func (s *DateTimePicker) confirmSelection(b *bot.Bot) {
func (s *DateTimePicker) hourButton(hour int) echotron.InlineKeyboardButton {
label := fmt.Sprintf("%02d", hour)
if s.isHourDisabled(hour) {
- return bot.Button("-", "empty")
+ return Button("-", "empty")
}
if s.selectedHour != nil && *s.selectedHour == hour {
label = "● " + label
}
- return bot.Button(label, fmt.Sprintf("%shour:%02d", dtpCallbackPrefix, hour))
+ return Button(label, fmt.Sprintf("%shour:%02d", dtpCallbackPrefix, hour))
}
func (s *DateTimePicker) minuteButton(minute int) echotron.InlineKeyboardButton {
label := fmt.Sprintf("%02d", minute)
if s.isMinuteDisabled(minute) {
- return bot.Button("-", "empty")
+ return Button("-", "empty")
}
if s.selectedMinute != nil && *s.selectedMinute == minute {
label = "● " + label
}
- return bot.Button(label, fmt.Sprintf("%smin:%02d", dtpCallbackPrefix, minute))
+ return Button(label, fmt.Sprintf("%smin:%02d", dtpCallbackPrefix, minute))
}
func (s *DateTimePicker) isHourDisabled(hour int) bool {
@@ -499,9 +499,9 @@ func (s *DateTimePicker) nextMonth() {
func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton {
now := time.Now().In(mskLocation)
if s.year == now.Year() && s.month == now.Month() {
- return bot.Button("-", "empty")
+ return Button("-", "empty")
}
- return bot.Button("←", dtpCallbackPrefix+"month_prev")
+ return Button("←", dtpCallbackPrefix+"month_prev")
}
func daysInMonth(year int, month time.Month) int {
diff --git a/telegram_bot/screens/helpers.go b/telegram_bot/screens/helpers.go
new file mode 100644
index 0000000..741888f
--- /dev/null
+++ b/telegram_bot/screens/helpers.go
@@ -0,0 +1,34 @@
+package screens
+
+import (
+ "github.com/NicoNex/echotron/v3"
+)
+
+var emptyKeyboard = Keyboard()
+
+func Button(text, data string) echotron.InlineKeyboardButton {
+ return echotron.InlineKeyboardButton{
+ Text: text,
+ CallbackData: data,
+ }
+}
+
+func URLButton(text, url string) echotron.InlineKeyboardButton {
+ return echotron.InlineKeyboardButton{
+ Text: text,
+ URL: url,
+ }
+}
+
+func Row(buttons ...echotron.InlineKeyboardButton) []echotron.InlineKeyboardButton { return buttons }
+
+func Keyboard(rows ...[]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
+ if len(rows) == 0 {
+ return echotron.InlineKeyboardMarkup{
+ InlineKeyboard: [][]echotron.InlineKeyboardButton{},
+ }
+ }
+ return echotron.InlineKeyboardMarkup{
+ InlineKeyboard: rows,
+ }
+}
diff --git a/telegram_bot/screens/main_menu.go b/telegram_bot/screens/main_menu.go
index a1dedfb..a1243e9 100644
--- a/telegram_bot/screens/main_menu.go
+++ b/telegram_bot/screens/main_menu.go
@@ -21,14 +21,14 @@ Smartpost — платформа для покупки и продажи рек
`
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
- keyboard := bot.Keyboard(
- bot.Row(
- bot.Button("Мои проекты", "my_projects"),
- bot.URLButton("Поддержка", "https://example.com"),
+ keyboard := Keyboard(
+ Row(
+ Button("Мои проекты", "my_projects"),
+ URLButton("Поддержка", "https://example.com"),
),
- bot.Row(
- bot.Button("Закупы", "purchases"),
- bot.URLButton("Дашборд", "https://example.com"),
+ Row(
+ Button("Закупы", "purchases"),
+ URLButton("Дашборд", "https://example.com"),
),
)
diff --git a/telegram_bot/screens/my_projects.go b/telegram_bot/screens/my_projects.go
index 21dd4ff..93efc5c 100644
--- a/telegram_bot/screens/my_projects.go
+++ b/telegram_bot/screens/my_projects.go
@@ -25,7 +25,7 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", Keyboard())
return
}
@@ -34,7 +34,7 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt)
if err != nil {
log.Error().Err(err).Msg("Failed to get workspaces")
- b.SendNew("❌ Не удалось загрузить workspace'ы", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить workspace'ы", Keyboard())
return
}
@@ -57,7 +57,7 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
workspace, err := b.Backend.CreateWorkspace(context.Background(), jwt, username)
if err != nil {
log.Error().Err(err).Msg("Failed to create workspace")
- b.SendNew("❌ Не удалось создать workspace", bot.Keyboard())
+ b.SendNew("❌ Не удалось создать workspace", Keyboard())
return
}
@@ -76,7 +76,7 @@ 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)
if err != nil {
log.Error().Err(err).Msg("Failed to get projects")
- b.SendNew("❌ Не удалось загрузить проекты", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить проекты", Keyboard())
return
}
@@ -122,13 +122,13 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
// Создаем слоты для проектов
var slots []echotron.InlineKeyboardButton
for _, project := range page.Items {
- slots = append(slots, bot.Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
+ 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, bot.Button(" ", "empty"))
+ slots = append(slots, Button(" ", "empty"))
}
}
@@ -142,7 +142,7 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
row = append(row, slots[i+1])
} else {
// Если нечетное количество, добавляем пустую кнопку
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
buttons = append(buttons, row)
@@ -152,18 +152,18 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
TotalPages: page.Pages,
- MiddleButtons: []echotron.InlineKeyboardButton{bot.Button("+ Добавить", "add_project")},
+ MiddleButtons: []echotron.InlineKeyboardButton{Button("+ Добавить", "add_project")},
}); navRow != nil {
buttons = append(buttons, navRow)
}
// Нижний ряд: Назад + Архив
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "menu"),
- bot.Button("≡ Архив", "archive"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "menu"),
+ Button("≡ Архив", "archive"),
))
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
@@ -189,8 +189,8 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(&MainMenu{}, bot.EditMessage)
case data == "archive":
- b.Edit("📦 Архив проектов\n\nЭта функция в разработке", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_projects")),
+ b.Edit("📦 Архив проектов\n\nЭта функция в разработке", Keyboard(
+ Row(Button("← Назад", "back_to_projects")),
))
case data == "add_project":
diff --git a/telegram_bot/screens/project_details.go b/telegram_bot/screens/project_details.go
index 40b5a91..ea8c1bc 100644
--- a/telegram_bot/screens/project_details.go
+++ b/telegram_bot/screens/project_details.go
@@ -20,8 +20,8 @@ type ProjectDetails struct {
func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
if s.Project == nil {
- b.SendNew("❌ Проект не найден", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_projects")),
+ b.SendNew("❌ Проект не найден", Keyboard(
+ Row(Button("← Назад", "back_to_projects")),
))
return
}
@@ -76,9 +76,9 @@ func (s *ProjectDetails) buildKeyboard() echotron.InlineKeyboardMarkup {
var buttons [][]echotron.InlineKeyboardButton
// Первый ряд: Креативы и Закупы
- buttons = append(buttons, bot.Row(
- bot.Button("Креативы", fmt.Sprintf("creatives:%s", s.Project.ID)),
- bot.Button("Закупы", fmt.Sprintf("purchases:%s", s.Project.ID)),
+ buttons = append(buttons, Row(
+ Button("Креативы", fmt.Sprintf("creatives:%s", s.Project.ID)),
+ Button("Закупы", fmt.Sprintf("purchases:%s", s.Project.ID)),
))
// Второй ряд: Тип вступления по умолчанию для закупов (с подтверждением)
@@ -102,17 +102,17 @@ func (s *ProjectDetails) buildKeyboard() echotron.InlineKeyboardMarkup {
}
linkTypeButtonText = fmt.Sprintf("Вступление: %s", currentType)
}
- buttons = append(buttons, bot.Row(
- bot.Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)),
+ buttons = append(buttons, Row(
+ Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)),
))
// Нижний ряд: Назад и Архивировать
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back_to_projects"),
- bot.Button("≡ Архивировать", fmt.Sprintf("archive:%s", s.Project.ID)),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back_to_projects"),
+ Button("≡ Архивировать", fmt.Sprintf("archive:%s", s.Project.ID)),
))
- return bot.Keyboard(buttons...)
+ return Keyboard(buttons...)
}
func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
@@ -181,8 +181,8 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
)
if err != nil {
- b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_project")),
+ b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), Keyboard(
+ Row(Button("← Назад", "back_to_project")),
))
return
}
@@ -195,8 +195,8 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
default:
// Для остальных кнопок показываем заглушку
- b.Edit(fmt.Sprintf("🚧 Функция в разработке\n\nCallback: %s", data), bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_project")),
+ b.Edit(fmt.Sprintf("🚧 Функция в разработке\n\nCallback: %s", data), Keyboard(
+ Row(Button("← Назад", "back_to_project")),
))
}
return
diff --git a/telegram_bot/screens/purchase_details.go b/telegram_bot/screens/purchase_details.go
index 8b5aa3b..f09abea 100644
--- a/telegram_bot/screens/purchase_details.go
+++ b/telegram_bot/screens/purchase_details.go
@@ -23,7 +23,7 @@ func (s *PurchaseDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return
}
@@ -40,7 +40,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
)
if err != nil {
log.Error().Err(err).Msg("Failed to get purchase")
- b.SendNew("❌ Не удалось загрузить закуп", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить закуп", Keyboard())
return
}
@@ -153,11 +153,11 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
var buttons [][]echotron.InlineKeyboardButton
// Кнопка назад
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back"),
))
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go
index f68b95c..0e77f9c 100644
--- a/telegram_bot/screens/purchase_optional_details.go
+++ b/telegram_bot/screens/purchase_optional_details.go
@@ -47,6 +47,7 @@ type PurchaseOptionalDetails struct {
CostCopy *CostEntry
CostBeforeCopy *float64
FormatCopy *string
+ ChannelEditMode string // "edit" или "copy"
BackState bot.State
}
@@ -58,6 +59,10 @@ type CostEntry struct {
func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
s.ensureDefaults()
if s.InputMode != "" {
+ if s.InputMode == "mode_select" {
+ s.renderModeSelect(b, mode)
+ return
+ }
if s.renderParamScreens(b, mode) {
return
}
@@ -72,30 +77,36 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
text += s.formatOptionalSummary()
var rows [][]echotron.InlineKeyboardButton
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.placementButton(),
s.costButton(),
))
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.paymentButton(),
s.typeButton(),
))
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.costBeforeButton(),
s.formatButton(),
))
- rows = append(rows, bot.Row(
+ rows = append(rows, Row(
s.commentButton(),
))
+
+ // Кнопка переключения режима (только если несколько каналов)
+ if len(s.Channels) > 1 {
+ rows = append(rows, Row(s.globalModeButton()))
+ }
+
if row := s.costBeforeDeleteRow(); row != nil {
rows = append(rows, row)
}
if row := s.commentDeleteRow(); row != nil {
rows = append(rows, row)
}
- rows = append(rows, bot.Row(bot.Button("Назад", "back"), bot.Button("Далее", "next")))
+ rows = append(rows, Row(Button("Назад", "back"), Button("Далее", "next")))
- keyboard := bot.Keyboard(rows...)
+ keyboard := Keyboard(rows...)
b.Render(text, keyboard, mode)
}
@@ -108,8 +119,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
switch u.CallbackQuery.Data {
case "opt_datetime":
- if len(s.Channels) > 1 {
- s.InputMode = "placement_scope"
+ // Открываем редактор в текущем режиме
+ if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
+ s.InputMode = "placement_channels"
s.Enter(b, bot.EditMessage)
} else {
s.setParamMode("placement", "common")
@@ -126,8 +138,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
}), bot.EditMessage)
case "opt_cost":
- if len(s.Channels) > 1 {
- s.InputMode = "cost_scope"
+ // Открываем редактор в текущем режиме
+ if s.CostMode == "per_channel" && len(s.Channels) > 1 {
+ s.InputMode = "cost_channels"
s.Enter(b, bot.EditMessage)
} else {
s.setParamMode("cost", "common")
@@ -139,8 +152,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage)
case "opt_format":
- if len(s.Channels) > 1 {
- s.InputMode = "format_scope"
+ // Открываем редактор в текущем режиме
+ if s.FormatMode == "per_channel" && len(s.Channels) > 1 {
+ s.InputMode = "format_channels"
s.Enter(b, bot.EditMessage)
} else {
s.setParamMode("format", "common")
@@ -168,8 +182,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage)
case "cost_before":
- if len(s.Channels) > 1 {
- s.InputMode = "cost_before_scope"
+ // Открываем редактор в текущем режиме
+ if s.CostBeforeMode == "per_channel" && len(s.Channels) > 1 {
+ s.InputMode = "cost_before_channels"
s.Enter(b, bot.EditMessage)
} else {
s.setParamMode("cost_before", "common")
@@ -210,7 +225,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return
}
s.createPurchase(b, jwt)
@@ -219,9 +234,57 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
b.SetState(s.BackState, bot.EditMessage)
}
default:
+ if u.CallbackQuery.Data == "noop" {
+ // Пустая кнопка - ничего не делаем
+ return
+ }
if s.handleParamCallback(b, u.CallbackQuery.Data) {
return
}
+ if u.CallbackQuery.Data == "toggle_global_mode" {
+ // Открываем меню выбора параметра для переключения режима
+ s.InputMode = "mode_select"
+ s.Enter(b, bot.EditMessage)
+ return
+ }
+ if strings.HasPrefix(u.CallbackQuery.Data, "channel_mode:") {
+ // Переключаем режим редактирования каналов
+ mode := strings.TrimPrefix(u.CallbackQuery.Data, "channel_mode:")
+ s.ChannelEditMode = mode
+ s.Enter(b, bot.EditMessage)
+ return
+ }
+ if strings.HasPrefix(u.CallbackQuery.Data, "toggle_param_mode:") {
+ // Переключаем режим конкретного параметра
+ param := strings.TrimPrefix(u.CallbackQuery.Data, "toggle_param_mode:")
+ currentMode := s.paramMode(param)
+ if currentMode == "common" {
+ s.setParamMode(param, "per_channel")
+ } else {
+ s.setParamMode(param, "common")
+ }
+ // Остаемся в меню выбора режима
+ s.Enter(b, bot.EditMessage)
+ return
+ }
+ if strings.HasPrefix(u.CallbackQuery.Data, "type:") {
+ value := strings.TrimPrefix(u.CallbackQuery.Data, "type:")
+ if value == "custom" {
+ s.InputMode = "type_custom"
+ s.Enter(b, bot.EditMessage)
+ return
+ }
+ // Нормализуем значение для БД
+ switch value {
+ case "self_promo":
+ s.PurchaseType = "самопиар"
+ case "standard":
+ s.PurchaseType = "стандарт"
+ }
+ s.InputMode = ""
+ s.Enter(b, bot.EditMessage)
+ return
+ }
if strings.HasPrefix(u.CallbackQuery.Data, "format:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "format:")
s.setFormatValue(value)
@@ -251,7 +314,7 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
}
switch s.InputMode {
- case "type":
+ case "type_custom":
s.PurchaseType = text
case "format_custom":
s.setFormatValue(text)
@@ -350,87 +413,104 @@ func (s *PurchaseOptionalDetails) costTypeLabel() string {
}
func (s *PurchaseOptionalDetails) placementButton() echotron.InlineKeyboardButton {
- if !s.hasPlacementValue() {
- return bot.Button("+ Дата размещения", "opt_datetime")
+ icon := "+"
+ if s.hasPlacementValue() {
+ icon = "✎"
}
- return bot.Button("✎ Дата размещения", "opt_datetime")
+ modeIcon := ""
+ if len(s.Channels) > 1 && s.PlacementMode == "per_channel" {
+ modeIcon = " 👥"
+ }
+ return Button(fmt.Sprintf("%s Дата размещения%s", icon, modeIcon), "opt_datetime")
}
func (s *PurchaseOptionalDetails) paymentButton() echotron.InlineKeyboardButton {
if s.PaymentDate == nil {
- return bot.Button("+ Дата оплаты", "opt_payment_date")
+ return Button("+ Дата оплаты", "opt_payment_date")
}
- return bot.Button("✎ Дата оплаты", "opt_payment_date")
+ return Button("✎ Дата оплаты", "opt_payment_date")
}
func (s *PurchaseOptionalDetails) costButton() echotron.InlineKeyboardButton {
- if !s.hasCostValue() {
- return bot.Button("+ Стоимость", "opt_cost")
+ icon := "+"
+ if s.hasCostValue() {
+ icon = "✎"
}
- return bot.Button("✎ Стоимость", "opt_cost")
+ modeIcon := ""
+ if len(s.Channels) > 1 && s.CostMode == "per_channel" {
+ modeIcon = " 👥"
+ }
+ return Button(fmt.Sprintf("%s Стоимость%s", icon, modeIcon), "opt_cost")
}
func (s *PurchaseOptionalDetails) costBeforeButton() echotron.InlineKeyboardButton {
- if !s.hasCostBeforeValue() {
- return bot.Button("+ До торга", "cost_before")
+ icon := "+"
+ if s.hasCostBeforeValue() {
+ icon = "✎"
}
- return bot.Button("✎ До торга", "cost_before")
+ modeIcon := ""
+ if len(s.Channels) > 1 && s.CostBeforeMode == "per_channel" {
+ modeIcon = " 👥"
+ }
+ return Button(fmt.Sprintf("%s До торга%s", icon, modeIcon), "cost_before")
}
func (s *PurchaseOptionalDetails) typeButton() echotron.InlineKeyboardButton {
if s.PurchaseType == "" {
- return bot.Button("+ Тип", "opt_type")
+ return Button("+ Тип", "opt_type")
}
- return bot.Button("✎ Тип", "opt_type")
+ return Button("✎ Тип", "opt_type")
}
func (s *PurchaseOptionalDetails) formatButton() echotron.InlineKeyboardButton {
- if !s.hasFormatValue() {
- return bot.Button("+ Формат", "opt_format")
+ icon := "+"
+ if s.hasFormatValue() {
+ icon = "✎"
}
- return bot.Button("✎ Формат", "opt_format")
+ modeIcon := ""
+ if len(s.Channels) > 1 && s.FormatMode == "per_channel" {
+ modeIcon = " 👥"
+ }
+ return Button(fmt.Sprintf("%s Формат%s", icon, modeIcon), "opt_format")
}
func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton {
if s.Comment == "" {
- return bot.Button("+ Комментарий", "opt_comment")
+ return Button("+ Комментарий", "opt_comment")
}
- return bot.Button("✎ Комментарий", "opt_comment")
+ return Button("✎ Комментарий", "opt_comment")
+}
+
+func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton {
+ return Button("⚙ Общие / По каналам", "toggle_global_mode")
}
func (s *PurchaseOptionalDetails) commentDeleteRow() []echotron.InlineKeyboardButton {
if s.Comment == "" {
return nil
}
- return bot.Row(bot.Button("⌫ Удалить комментарий", "delete_comment"))
+ return Row(Button("⌫ Удалить комментарий", "delete_comment"))
}
func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboardButton {
if s.CostBeforeBargain == nil || s.CostBeforeMode == "per_channel" {
return nil
}
- return bot.Row(bot.Button("⌫ Удалить до торга", "delete_cost_before"))
+ return Row(Button("⌫ Удалить до торга", "delete_cost_before"))
}
func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderMode) {
text := "… › Создание › Дополнительно\n\n"
switch s.InputMode {
case "type":
- text += "Введите тип закупа\n\nНапример: вп или стандарт"
- case "cost_select":
- text = "Страница ввода стоимости\n\n"
- if s.CurrentChannel != "" {
- text += fmt.Sprintf("Канал: @%s\n\n", s.CurrentChannel)
- }
- text += "Ставка СРМ / Фикс цена (по умолчанию фикс)\n\n"
- costValue := s.formatCostValue()
- if s.CurrentChannel != "" {
- costValue = s.formatCostForChannel(s.CurrentChannel)
- }
- keyboard := bot.Keyboard(
- bot.Row(bot.Button(fmt.Sprintf("Тип: %s", s.costTypeLabelForCurrent()), "cost_type_toggle")),
- bot.Row(bot.Button(fmt.Sprintf("Стоимость: %s", costValue), "cost_value")),
- bot.Row(bot.Button("Назад", s.backAction())),
+ text += "Выберите тип закупа"
+ keyboard := Keyboard(
+ Row(
+ Button("Самопиар", "type:self_promo"),
+ Button("Стандарт", "type:standard"),
+ ),
+ Row(Button("Другое", "type:custom")),
+ Row(Button("← Назад", "back_to_optional")),
)
if mode == bot.EditMessage {
b.Edit(text, keyboard)
@@ -438,11 +518,26 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
b.SendNew(text, keyboard)
}
return
+ case "type_custom":
+ text += "Введите тип закупа\n\nНапример: вп"
case "cost_value":
- text += "Ввод стоимости (СРМ / фикс)\n\nНапример: 15000"
+ text += "Ввод стоимости\n\nНапример: 15000"
if s.CurrentChannel != "" {
text += fmt.Sprintf("\n\nКанал: @%s", s.CurrentChannel)
}
+
+ typeLabel := s.costTypeLabelForCurrent()
+ keyboard := Keyboard(
+ Row(Button(fmt.Sprintf("Тип: %s", typeLabel), "cost_type_toggle")),
+ Row(Button("← Назад", s.backAction())),
+ )
+
+ if mode == bot.EditMessage {
+ b.Edit(text, keyboard)
+ } else {
+ b.SendNew(text, keyboard)
+ }
+ return
case "cost_before_value":
text += "Ввод стоимости до торга\n\nНапример: 20000"
if s.CurrentChannel != "" {
@@ -454,25 +549,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
text += fmt.Sprintf("Канал: @%s\n\n", s.CurrentChannel)
}
text += "Выберите формат\n\n"
- keyboard := bot.Keyboard(
- bot.Row(
- bot.Button("1 / 24", "format:1 / 24"),
- bot.Button("1/48", "format:1/48"),
- bot.Button("1 / 72", "format:1 / 72"),
+ keyboard := Keyboard(
+ Row(
+ Button("1 / 24", "format:1 / 24"),
+ Button("1/48", "format:1/48"),
+ Button("1 / 72", "format:1 / 72"),
),
- bot.Row(
- bot.Button("1 / (7 дней)", "format:1 / (7 дней)"),
- bot.Button("1 / (30 дней)", "format:1 / (30 дней)"),
- bot.Button("1 / (без удаления)", "format:1 / (без удаления)"),
+ Row(
+ Button("1 / (7 дней)", "format:1 / (7 дней)"),
+ Button("1 / (30 дней)", "format:1 / (30 дней)"),
+ Button("1 / (без удаления)", "format:1 / (без удаления)"),
),
- bot.Row(
- bot.Button("2 / 24", "format:2 / 24"),
- bot.Button("2/48", "format:2/48"),
- bot.Button("2/72", "format:2/72"),
+ Row(
+ Button("2 / 24", "format:2 / 24"),
+ Button("2/48", "format:2/48"),
+ Button("2/72", "format:2/72"),
),
- bot.Row(
- bot.Button("Назад", s.backAction()),
- bot.Button("Свой формат", "format_custom"),
+ Row(
+ Button("Назад", s.backAction()),
+ Button("Свой формат", "format_custom"),
),
)
if mode == bot.EditMessage {
@@ -491,8 +586,8 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
return
}
- keyboard := bot.Keyboard(
- bot.Row(bot.Button("← Назад", s.backAction())),
+ keyboard := Keyboard(
+ Row(Button("← Назад", s.backAction())),
)
if mode == bot.EditMessage {
@@ -502,29 +597,70 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
}
}
+func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMode) {
+ text := "… › Создание › Дополнительно › Выбор режима\n\n"
+ text += "⚙ Переключить режим параметра\n\n"
+
+ // Показываем текущую информацию о параметрах
+ text += s.formatOptionalSummary()
+ text += "\n\n"
+
+ text += "Выберите параметр для изменения режима:\n\n"
+
+ var rows [][]echotron.InlineKeyboardButton
+
+ // Дата размещения
+ placementLabel := "Дата размещения: "
+ if s.PlacementMode == "per_channel" {
+ placementLabel += "для каждого"
+ } else {
+ placementLabel += "общий"
+ }
+ rows = append(rows, Row(Button(placementLabel, "toggle_param_mode:placement")))
+
+ // Стоимость
+ costLabel := "Стоимость: "
+ if s.CostMode == "per_channel" {
+ costLabel += "для каждого"
+ } else {
+ costLabel += "общий"
+ }
+ rows = append(rows, Row(Button(costLabel, "toggle_param_mode:cost")))
+
+ // До торга
+ costBeforeLabel := "До торга: "
+ if s.CostBeforeMode == "per_channel" {
+ costBeforeLabel += "для каждого"
+ } else {
+ costBeforeLabel += "общий"
+ }
+ rows = append(rows, Row(Button(costBeforeLabel, "toggle_param_mode:cost_before")))
+
+ // Формат
+ formatLabel := "Формат: "
+ if s.FormatMode == "per_channel" {
+ formatLabel += "для каждого"
+ } else {
+ formatLabel += "общий"
+ }
+ rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format")))
+
+ rows = append(rows, Row(Button("← Назад", "back_to_optional")))
+
+ b.Render(text, Keyboard(rows...), mode)
+}
+
func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.RenderMode) bool {
switch s.InputMode {
- case "placement_scope":
- s.renderParamScope(b, mode, "placement")
- return true
case "placement_channels":
s.renderParamChannels(b, mode, "placement")
return true
- case "cost_scope":
- s.renderParamScope(b, mode, "cost")
- return true
case "cost_channels":
s.renderParamChannels(b, mode, "cost")
return true
- case "cost_before_scope":
- s.renderParamScope(b, mode, "cost_before")
- return true
case "cost_before_channels":
s.renderParamChannels(b, mode, "cost_before")
return true
- case "format_scope":
- s.renderParamScope(b, mode, "format")
- return true
case "format_channels":
s.renderParamChannels(b, mode, "format")
return true
@@ -533,31 +669,6 @@ func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.Render
}
}
-func (s *PurchaseOptionalDetails) renderParamScope(b *bot.Bot, mode bot.RenderMode, param string) {
- title := s.paramTitle(param)
- text := "… › Создание › Дополнительно\n\n"
- text += fmt.Sprintf("%s\n\n", title)
- text += "Выберите режим\n\n"
-
- commonLabel := "Общий"
- perLabel := "Для каждого"
- if s.paramMode(param) == "common" {
- commonLabel = "● " + commonLabel
- } else {
- perLabel = "● " + perLabel
- }
-
- rows := [][]echotron.InlineKeyboardButton{
- bot.Row(
- bot.Button(commonLabel, fmt.Sprintf("param_mode_common:%s", param)),
- bot.Button(perLabel, fmt.Sprintf("param_mode_per:%s", param)),
- ),
- bot.Row(bot.Button("← Назад", "back_to_optional")),
- }
-
- b.Render(text, bot.Keyboard(rows...), mode)
-}
-
func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.RenderMode, param string) {
s.CurrentParam = param
text := "… › Создание › Дополнительно\n\n"
@@ -568,7 +679,7 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende
total := len(s.Channels)
if total == 0 {
text += "\nКаналы не выбраны"
- b.Render(text, bot.Keyboard(bot.Row(bot.Button("← Назад", "back_to_optional"))), mode)
+ b.Render(text, Keyboard(Row(Button("← Назад", "back_to_optional"))), mode)
return
}
@@ -577,16 +688,74 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende
}
start, end := ui.GetPageBounds(s.ParamPage, perPage, total)
+ // Устанавливаем режим по умолчанию
+ if s.ChannelEditMode == "" {
+ s.ChannelEditMode = "edit"
+ }
+
var rows [][]echotron.InlineKeyboardButton
+
+ // Кнопки переключения режима
+ editLabel := "Редактирование"
+ copyLabel := "Копирование"
+ if s.ChannelEditMode == "edit" {
+ editLabel = "● " + editLabel
+ } else {
+ copyLabel = "● " + copyLabel
+ }
+ rows = append(rows, Row(
+ Button(editLabel, "channel_mode:edit"),
+ Button(copyLabel, "channel_mode:copy"),
+ ))
+
+ // Список каналов с кнопками в зависимости от режима
for i := start; i < end; i++ {
label := fmt.Sprintf("%d", i+1)
- rows = append(rows, bot.Row(
- bot.Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)),
- bot.Button("⧉", fmt.Sprintf("param_copy:%s:%d", param, i)),
- bot.Button("⇲", fmt.Sprintf("param_paste:%s:%d", param, i)),
- bot.Button("Изменить", fmt.Sprintf("param_edit:%s:%d", param, i)),
- bot.Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)),
- ))
+ channelUsername := s.Channels[i].Username
+
+ if s.ChannelEditMode == "copy" {
+ // Режим копирования
+ var channelRow []echotron.InlineKeyboardButton
+ channelRow = append(channelRow, Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)))
+ channelRow = append(channelRow, Button("⧉", fmt.Sprintf("param_copy:%s:%d", param, i)))
+
+ // Кнопка вставки - только если есть данные в буфере
+ if s.hasCopyBuffer(param) {
+ channelRow = append(channelRow, Button("⇲", fmt.Sprintf("param_paste:%s:%d", param, i)))
+ } else {
+ channelRow = append(channelRow, Button(" ", "noop"))
+ }
+
+ // Кнопка удаления - только если есть данные у канала
+ if s.hasChannelValue(param, channelUsername) {
+ channelRow = append(channelRow, Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)))
+ } else {
+ channelRow = append(channelRow, Button(" ", "noop"))
+ }
+
+ rows = append(rows, channelRow)
+ } else {
+ // Обычный режим редактирования
+ var channelRow []echotron.InlineKeyboardButton
+ channelRow = append(channelRow, Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)))
+
+ // Кнопка "Добавить" или "Изменить" в зависимости от наличия данных
+ hasValue := s.hasChannelValue(param, channelUsername)
+ if hasValue {
+ channelRow = append(channelRow, Button("Изменить", fmt.Sprintf("param_edit:%s:%d", param, i)))
+ } else {
+ channelRow = append(channelRow, Button("Добавить", fmt.Sprintf("param_edit:%s:%d", param, i)))
+ }
+
+ // Кнопка удаления - только если есть данные у канала
+ if hasValue {
+ channelRow = append(channelRow, Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)))
+ } else {
+ channelRow = append(channelRow, Button(" ", "noop"))
+ }
+
+ rows = append(rows, channelRow)
+ }
}
pages := ui.CalculatePages(total, perPage)
@@ -597,12 +766,11 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende
rows = append(rows, navRow)
}
- rows = append(rows, bot.Row(bot.Button("Применить ко всем", fmt.Sprintf("param_apply_all:%s", param))))
- rows = append(rows, bot.Row(
- bot.Button("← Назад", fmt.Sprintf("param_back:%s", param)),
+ rows = append(rows, Row(
+ Button("← Назад", fmt.Sprintf("param_back:%s", param)),
))
- b.Render(text, bot.Keyboard(rows...), mode)
+ b.Render(text, Keyboard(rows...), mode)
}
func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string {
@@ -616,20 +784,6 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) bool {
switch {
- case strings.HasPrefix(data, "param_mode_common:"):
- param := strings.TrimPrefix(data, "param_mode_common:")
- s.setParamMode(param, "common")
- s.CurrentChannel = ""
- s.CurrentParam = param
- s.openCommonEditor(b, param)
- return true
- case strings.HasPrefix(data, "param_mode_per:"):
- param := strings.TrimPrefix(data, "param_mode_per:")
- s.setParamMode(param, "per_channel")
- s.CurrentParam = param
- s.InputMode = param + "_channels"
- s.Enter(b, bot.EditMessage)
- return true
case strings.HasPrefix(data, "param_edit:"):
param, index, ok := s.parseParamIndex(data, "param_edit:")
if !ok {
@@ -686,11 +840,6 @@ func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) b
}
s.Enter(b, bot.EditMessage)
return true
- case strings.HasPrefix(data, "param_apply_all:"):
- param := strings.TrimPrefix(data, "param_apply_all:")
- s.applyParamToAll(param)
- s.Enter(b, bot.EditMessage)
- return true
case data == "prev" && strings.HasSuffix(s.InputMode, "_channels"):
if s.ParamPage > 0 {
s.ParamPage--
@@ -702,8 +851,8 @@ func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) b
s.Enter(b, bot.EditMessage)
return true
case strings.HasPrefix(data, "param_back:"):
- param := strings.TrimPrefix(data, "param_back:")
- s.InputMode = param + "_scope"
+ // Возвращаемся на главный экран
+ s.InputMode = ""
s.Enter(b, bot.EditMessage)
return true
}
@@ -734,7 +883,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
BackState: s,
}), bot.EditMessage)
case "cost":
- s.InputMode = "cost_select"
+ s.InputMode = "cost_value"
s.ReturnMode = ""
s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
@@ -766,7 +915,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, username
BackState: s,
}), bot.EditMessage)
case "cost":
- s.InputMode = "cost_select"
+ s.InputMode = "cost_value"
s.ReturnMode = "cost_channels"
s.CurrentChannel = username
s.Enter(b, bot.EditMessage)
@@ -1084,6 +1233,40 @@ func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(username string) st
return fmt.Sprintf("%.0f₽", *value)
}
+func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
+ switch param {
+ case "placement":
+ return s.PlacementCopy != nil
+ case "cost":
+ return s.CostCopy != nil
+ case "cost_before":
+ return s.CostBeforeCopy != nil
+ case "format":
+ return s.FormatCopy != nil
+ default:
+ return false
+ }
+}
+
+func (s *PurchaseOptionalDetails) hasChannelValue(param, username string) bool {
+ switch param {
+ case "placement":
+ value, ok := s.PlacementByChannel[username]
+ return ok && value != nil
+ case "cost":
+ entry, ok := s.CostByChannel[username]
+ return ok && entry.Value != nil
+ case "cost_before":
+ value, ok := s.CostBeforeByChannel[username]
+ return ok && value != nil
+ case "format":
+ value, ok := s.FormatByChannel[username]
+ return ok && value != ""
+ default:
+ return false
+ }
+}
+
func (s *PurchaseOptionalDetails) hasPlacementValue() bool {
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
for _, value := range s.PlacementByChannel {
@@ -1209,16 +1392,16 @@ func (s *PurchaseOptionalDetails) backAction() string {
func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
if len(s.Channels) == 0 {
- b.SendNew("❌ Добавьте хотя бы один канал", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back")),
+ b.SendNew("❌ Добавьте хотя бы один канал", Keyboard(
+ Row(Button("← Назад", "back")),
))
return
}
purchaseType, ok := s.normalizePurchaseType()
if !ok {
- b.SendNew("❌ Тип закупа: используйте «самопиар» или «стандарт»", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back")),
+ b.SendNew("❌ Тип закупа: используйте «самопиар» или «стандарт»", Keyboard(
+ Row(Button("← Назад", "back")),
))
return
}
@@ -1256,8 +1439,8 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
)
if err != nil {
log.Error().Err(err).Msg("Failed to create purchase")
- b.SendNew("❌ Не удалось создать закуп", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back")),
+ b.SendNew("❌ Не удалось создать закуп", Keyboard(
+ Row(Button("← Назад", "back")),
))
return
}
diff --git a/telegram_bot/screens/purchases.go b/telegram_bot/screens/purchases.go
index 245ef67..fc76e9c 100644
--- a/telegram_bot/screens/purchases.go
+++ b/telegram_bot/screens/purchases.go
@@ -20,7 +20,7 @@ func (s *Purchases) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return
}
@@ -36,7 +36,7 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
)
if err != nil {
log.Error().Err(err).Msg("Failed to get purchases")
- b.SendNew("❌ Не удалось загрузить закупы", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить закупы", Keyboard())
return
}
@@ -89,23 +89,23 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
totalChannels,
)
- buttons = append(buttons, bot.Row(
- bot.Button(buttonText, fmt.Sprintf("purchase:%s", purchase.ID)),
+ buttons = append(buttons, Row(
+ Button(buttonText, fmt.Sprintf("purchase:%s", purchase.ID)),
))
}
}
// Кнопка создания нового закупа
- buttons = append(buttons, bot.Row(
- bot.Button("+ Создать закуп", "add_purchase"),
+ buttons = append(buttons, Row(
+ Button("+ Создать закуп", "add_purchase"),
))
// Кнопка назад
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back"),
))
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
diff --git a/telegram_bot/screens/select_channels_for_purchase.go b/telegram_bot/screens/select_channels_for_purchase.go
index 1b480c8..20bd182 100644
--- a/telegram_bot/screens/select_channels_for_purchase.go
+++ b/telegram_bot/screens/select_channels_for_purchase.go
@@ -76,12 +76,12 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
if ch.PlannedCost != nil {
buttonText = fmt.Sprintf("@%s — %.0f₽", ch.Username, *ch.PlannedCost)
}
- slots = append(slots, bot.Button(buttonText, fmt.Sprintf("remove_channel:%d", i)))
+ slots = append(slots, Button(buttonText, fmt.Sprintf("remove_channel:%d", i)))
}
if len(s.Channels) > channelsPerPage {
for len(slots) < channelsPerPage {
- slots = append(slots, bot.Button(" ", "empty"))
+ slots = append(slots, Button(" ", "empty"))
}
}
@@ -90,7 +90,7 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
- row = append(row, bot.Button(" ", "empty"))
+ row = append(row, Button(" ", "empty"))
}
buttons = append(buttons, row)
}
@@ -116,17 +116,17 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
// Кнопки навигации
if len(s.Channels) > 0 {
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back"),
- bot.Button("→ Далее", "next_step"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back"),
+ Button("→ Далее", "next_step"),
))
} else {
- buttons = append(buttons, bot.Row(
- bot.Button("← Назад", "back"),
+ buttons = append(buttons, Row(
+ Button("← Назад", "back"),
))
}
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
@@ -152,8 +152,8 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
case "next_step":
if len(s.Channels) == 0 {
- b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_select")),
+ b.Edit("❌ Добавьте хотя бы один канал", Keyboard(
+ Row(Button("← Назад", "back_to_select")),
))
return
}
@@ -239,8 +239,8 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
if err != nil {
log.Error().Err(err).Msg("Failed to create purchase")
- b.Edit("❌ Не удалось создать закуп\n\nВозможные причины:\n• Один из каналов не найден\n• Ошибка сервера", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_select")),
+ b.Edit("❌ Не удалось создать закуп\n\nВозможные причины:\n• Один из каналов не найден\n• Ошибка сервера", Keyboard(
+ Row(Button("← Назад", "back_to_select")),
))
return
}
diff --git a/telegram_bot/screens/select_workspace.go b/telegram_bot/screens/select_workspace.go
index f6fcd2c..7be7080 100644
--- a/telegram_bot/screens/select_workspace.go
+++ b/telegram_bot/screens/select_workspace.go
@@ -20,20 +20,20 @@ func (s *SelectWorkspace) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
- b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
+ b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return
}
workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt)
if err != nil {
log.Error().Err(err).Msg("Failed to get workspaces")
- b.SendNew("❌ Не удалось загрузить список workspace'ов", bot.Keyboard())
+ b.SendNew("❌ Не удалось загрузить список workspace'ов", Keyboard())
return
}
if len(workspaces) == 0 {
- b.SendNew("У вас пока нет workspace'ов", bot.Keyboard(
- bot.Row(bot.Button("🏠 Главное меню", "back")),
+ b.SendNew("У вас пока нет workspace'ов", Keyboard(
+ Row(Button("🏠 Главное меню", "back")),
))
return
}
@@ -45,15 +45,15 @@ func (s *SelectWorkspace) Enter(b *bot.Bot, mode bot.RenderMode) {
// Создаем кнопки для каждого workspace
for _, ws := range workspaces {
- buttons = append(buttons, bot.Row(
- bot.Button(ws.Name, fmt.Sprintf("select_workspace:%s", ws.ID)),
+ buttons = append(buttons, Row(
+ Button(ws.Name, fmt.Sprintf("select_workspace:%s", ws.ID)),
))
}
// Кнопка отмены
- buttons = append(buttons, bot.Row(bot.Button("❌ Отмена", "cancel")))
+ buttons = append(buttons, Row(Button("❌ Отмена", "cancel")))
- keyboard := bot.Keyboard(buttons...)
+ keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
@@ -81,7 +81,7 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
workspaceID := parts[1]
// Показываем индикатор загрузки
- b.Edit("⏳ Добавляем канал в workspace...", bot.Keyboard())
+ b.Edit("⏳ Добавляем канал в workspace...", Keyboard())
// Вызываем API для привязки канала к workspace
project, err := b.Backend.AttachChannelToWorkspace(
@@ -92,8 +92,8 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
)
if err != nil {
log.Error().Err(err).Str("channel_id", s.ChannelID).Str("workspace_id", workspaceID).Msg("Failed to attach channel to workspace")
- b.Edit("❌ Не удалось добавить канал в workspace", bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_select")),
+ b.Edit("❌ Не удалось добавить канал в workspace", Keyboard(
+ Row(Button("← Назад", "back_to_select")),
))
return
}
@@ -108,9 +108,9 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
project.Status,
)
- b.Edit(successText, bot.Keyboard(
- bot.Row(bot.Button("📋 Мои проекты", "my_projects")),
- bot.Row(bot.Button("🏠 Главное меню", "main_menu")),
+ b.Edit(successText, Keyboard(
+ Row(Button("📋 Мои проекты", "my_projects")),
+ Row(Button("🏠 Главное меню", "main_menu")),
))
case data == "back_to_select":
diff --git a/telegram_bot/ui/pagination.go b/telegram_bot/ui/pagination.go
index 45c5bb3..ab3fa3b 100644
--- a/telegram_bot/ui/pagination.go
+++ b/telegram_bot/ui/pagination.go
@@ -3,7 +3,6 @@ package ui
import (
"fmt"
- "example.com/m/bot"
"github.com/NicoNex/echotron/v3"
)
@@ -42,7 +41,10 @@ func BuildNavigationRow(config PaginationConfig) []echotron.InlineKeyboardButton
// Левая стрелка (только если есть предыдущая страница)
if totalPages > 1 && config.CurrentPage > 0 {
- row = append(row, bot.Button("←", prevCallback))
+ row = append(row, echotron.InlineKeyboardButton{
+ Text: "←",
+ CallbackData: prevCallback,
+ })
}
// Средние кнопки (если есть)
@@ -52,7 +54,10 @@ func BuildNavigationRow(config PaginationConfig) []echotron.InlineKeyboardButton
// Правая стрелка (только если есть следующая страница)
if totalPages > 1 && config.CurrentPage+1 < totalPages {
- row = append(row, bot.Button("→", nextCallback))
+ row = append(row, echotron.InlineKeyboardButton{
+ Text: "→",
+ CallbackData: nextCallback,
+ })
}
if len(row) == 0 {