From 9fbb6fdbbf78e76eeaa22f8f45a96abee519ea82 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Fri, 9 Jan 2026 12:23:02 +0300 Subject: [PATCH] =?UTF-8?q?=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- telegram_bot/bot/bot.go | 32 +- .../screens/accept_workspace_invite.go | 40 +- telegram_bot/screens/add_creative.go | 83 ++- telegram_bot/screens/add_project.go | 4 +- telegram_bot/screens/add_purchase.go | 50 +- telegram_bot/screens/creative_details.go | 61 ++- telegram_bot/screens/creative_editor.go | 28 +- telegram_bot/screens/creatives.go | 24 +- telegram_bot/screens/date_time_picker.go | 88 ++-- telegram_bot/screens/helpers.go | 34 ++ telegram_bot/screens/main_menu.go | 14 +- telegram_bot/screens/my_projects.go | 28 +- telegram_bot/screens/project_details.go | 30 +- telegram_bot/screens/purchase_details.go | 10 +- .../screens/purchase_optional_details.go | 479 ++++++++++++------ telegram_bot/screens/purchases.go | 18 +- .../screens/select_channels_for_purchase.go | 26 +- telegram_bot/screens/select_workspace.go | 28 +- telegram_bot/ui/pagination.go | 11 +- 19 files changed, 635 insertions(+), 453 deletions(-) create mode 100644 telegram_bot/screens/helpers.go 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 {