улучшение ui

This commit is contained in:
Artem Tsyrulnikov
2026-01-09 12:23:02 +03:00
parent 40c7be8470
commit 9fbb6fdbbf
19 changed files with 635 additions and 453 deletions

View File

@@ -12,33 +12,6 @@ import (
"github.com/rs/zerolog/log" "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 { type Exec struct {
handled bool handled bool
transitioned bool transitioned bool
@@ -363,7 +336,10 @@ func (b *Bot) Update(u *echotron.Update) {
} }
func (b *Bot) cleanupKeyboard(messageID int) { func (b *Bot) cleanupKeyboard(messageID int) {
emptyKeyboard := Keyboard() emptyKeyboard := echotron.InlineKeyboardMarkup{
InlineKeyboard: [][]echotron.InlineKeyboardButton{},
}
_, err := b.EditMessageReplyMarkup( _, err := b.EditMessageReplyMarkup(
echotron.NewMessageID(b.ChatID, messageID), echotron.NewMessageID(b.ChatID, messageID),
&echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard}, &echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard},

View File

@@ -5,7 +5,6 @@ import (
"example.com/m/bot" "example.com/m/bot"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log"
) )
type AcceptWorkspaceInvite struct { type AcceptWorkspaceInvite struct {
@@ -18,38 +17,29 @@ const msgAcceptWorkspaceInviteSuccessfully = `
` `
func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *AcceptWorkspaceInvite) Enter(b *bot.Bot, mode bot.RenderMode) {
// Получаем JWT токен jwt := b.Session.JWT
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID) if jwt == "" {
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", emptyKeyboard)
return
}
err := b.Backend.AcceptWorkspaceInvite(context.Background(), jwt, s.InviteID)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get JWT") b.SendNew("❌ Не удалось принять приглашение. Возможно, оно уже было использовано.", emptyKeyboard)
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
return return
} }
// Принимаем приглашение kb := Keyboard(
err = b.Backend.AcceptWorkspaceInvite(context.Background(), jwt, s.InviteID) Row(Button("Главное меню", "menu")),
if err != nil { )
log.Error().Err(err).Str("invite_id", s.InviteID).Msg("Failed to accept workspace invite")
b.SendNew("❌ Не удалось принять приглашение. Возможно, оно уже было использовано.", bot.Keyboard()) b.SendNew(msgAcceptWorkspaceInviteSuccessfully, kb)
return
} }
b.SendNew(msgAcceptWorkspaceInviteSuccessfully, bot.Keyboard( func (s *AcceptWorkspaceInvite) HandleCallback(b *bot.Bot, u *echotron.Update) {}
bot.Row(bot.Button("🏠 Главное меню", "menu")),
))
}
func (s *AcceptWorkspaceInvite) HandleCallback(b *bot.Bot, u *echotron.Update) { func (s *AcceptWorkspaceInvite) HandleMessage(_ *bot.Bot, _ *echotron.Update) {}
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
if u.CallbackQuery.Data == "menu" { func (s *AcceptWorkspaceInvite) Handle(b *bot.Bot, _ *echotron.Update) {
b.SetState(&MainMenu{}, bot.EditMessage) b.SetState(&MainMenu{}, bot.EditMessage)
} }
return
}
func (s *AcceptWorkspaceInvite) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *AcceptWorkspaceInvite) Handle(_ *bot.Bot, _ *echotron.Update) { return }

View File

@@ -23,8 +23,8 @@ type AddCreative struct {
WaitingForTextEdit bool // Ждем редактирования текста креатива WaitingForTextEdit bool // Ждем редактирования текста креатива
} }
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) { const msgWaitCreative = `
text := `<i>… Креативы Добавление</i> <i>… Креативы Добавление</i>
<b> Добавить креатив</b> <b> Добавить креатив</b>
@@ -32,15 +32,13 @@ func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
• Обрабатываем любое сообщение Telegram • Обрабатываем любое сообщение Telegram
• Отправляйте сразу оформленный пост • Отправляйте сразу оформленный пост
На следующем шаге можно добавить кнопки или изменить контент` На следующем шаге можно добавить кнопки или изменить контент
`
buttons := [][]echotron.InlineKeyboardButton{ func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
bot.Row(bot.Button("« Отмена", "cancel")), kb := Keyboard(Row(Button("« Отмена", "cancel")))
}
keyboard := bot.Keyboard(buttons...)
s.WaitingForCreative = true s.WaitingForCreative = true
b.Render(text, keyboard, mode) b.Render(msgWaitCreative, kb, mode)
} }
func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) { func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
@@ -69,7 +67,7 @@ func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
<b>Пример:</b> <b>Пример:</b>
<i>Присоединяйтесь к нашему каналу! <i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_edit")), Row(Button("« Отмена", "cancel_edit")),
}) })
case data == "add_button": case data == "add_button":
@@ -86,11 +84,11 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
• Написать нам • Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{ <i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row( Row(
bot.Button("✓ Ссылка на канал", "button_type_invite"), Button("✓ Ссылка на канал", "button_type_invite"),
bot.Button("Своя ссылка", "button_type_custom"), Button("Своя ссылка", "button_type_custom"),
), ),
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
case data == "add_media": case data == "add_media":
@@ -102,7 +100,7 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
Отправьте <b>фото, видео или GIF</b> Отправьте <b>фото, видео или GIF</b>
<i>Это медиа будет отображаться в креативе вместе с текстом</i>`, [][]echotron.InlineKeyboardButton{ <i>Это медиа будет отображаться в креативе вместе с текстом</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_media")), Row(Button("« Отмена", "cancel_media")),
}) })
case data == "delete_media": case data == "delete_media":
@@ -150,11 +148,11 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
• Написать нам • Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{ <i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row( Row(
bot.Button("✓ Ссылка на канал", "button_type_invite"), Button("✓ Ссылка на канал", "button_type_invite"),
bot.Button("Своя ссылка", "button_type_custom"), Button("Своя ссылка", "button_type_custom"),
), ),
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
case data == "button_type_custom": case data == "button_type_custom":
@@ -168,11 +166,11 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
• Написать нам • Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{ <i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row( Row(
bot.Button("Ссылка на канал", "button_type_invite"), Button("Ссылка на канал", "button_type_invite"),
bot.Button("✓ Своя ссылка", "button_type_custom"), Button("✓ Своя ссылка", "button_type_custom"),
), ),
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
case data == "cancel_edit": case data == "cancel_edit":
@@ -321,7 +319,7 @@ URL должен начинаться с <code>https://</code>
<i>Например:</i> <i>Например:</i>
<code>https://example.com</code> <code>https://example.com</code>
<code>https://t.me/yourchannel</code>`, [][]echotron.InlineKeyboardButton{ <code>https://t.me/yourchannel</code>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
} else { } else {
s.Buttons = append(s.Buttons, InlineButton{ s.Buttons = append(s.Buttons, InlineButton{
@@ -359,7 +357,7 @@ URL должен начинаться с <code>https://</code>
URL должен начинаться с <code>https://</code> URL должен начинаться с <code>https://</code>
Попробуйте ещё раз.`, [][]echotron.InlineKeyboardButton{ Попробуйте ещё раз.`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
return return
} }
@@ -472,23 +470,20 @@ func (s *AddCreative) extractCreativeFromMessage(message *echotron.Message) {
// generateCreativeName генерирует название из текста или дефолтное // generateCreativeName генерирует название из текста или дефолтное
func (s *AddCreative) generateCreativeName() string { func (s *AddCreative) generateCreativeName() string {
// Если есть текст, берем первые слова
if s.Text != nil && *s.Text != "" { if s.Text != nil && *s.Text != "" {
// Удаляем ВСЕ HTML теги с помощью regex // Удаляем ВСЕ HTML теги с помощью regex
re := regexp.MustCompile(`<[^>]*>`) re := regexp.MustCompile(`<[^>]*>`)
cleanText := re.ReplaceAllString(*s.Text, "") cleanText := re.ReplaceAllString(*s.Text, "")
// Берем первые 50 символов
runes := []rune(strings.TrimSpace(cleanText)) runes := []rune(strings.TrimSpace(cleanText))
if len(runes) > 50 { if len(runes) > 10 {
return string(runes[:50]) + "..." return string(runes[:10]) + "..."
} }
if len(runes) > 0 { if len(runes) > 0 {
return string(runes) return string(runes)
} }
} }
// Дефолтное название
return "Новый креатив" return "Новый креатив"
} }
@@ -505,16 +500,16 @@ func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][
var buttons [][]echotron.InlineKeyboardButton var buttons [][]echotron.InlineKeyboardButton
// Кнопки редактирования // Кнопки редактирования
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("✎ Текст", "edit_text"), Button("✎ Текст", "edit_text"),
bot.Button(" Медиа", "add_media"), Button(" Медиа", "add_media"),
)) ))
buttons = append(buttons, bot.Row(bot.Button(" Добавить кнопку", "add_button"))) buttons = append(buttons, Row(Button(" Добавить кнопку", "add_button")))
// Кнопка удаления медиа (если медиа добавлено) // Кнопка удаления медиа (если медиа добавлено)
if s.MediaFileID != "" { if s.MediaFileID != "" {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("⌫ Убрать медиа", "delete_media"), Button("⌫ Убрать медиа", "delete_media"),
)) ))
} }
@@ -522,7 +517,7 @@ func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][
if len(s.Buttons) > 0 { if len(s.Buttons) > 0 {
var row []echotron.InlineKeyboardButton var row []echotron.InlineKeyboardButton
for i, btn := range s.Buttons { for i, btn := range s.Buttons {
row = append(row, bot.Button( row = append(row, Button(
fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text), fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text),
fmt.Sprintf("delete_button:%d", i), fmt.Sprintf("delete_button:%d", i),
)) ))
@@ -540,13 +535,13 @@ func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][
// Финальные кнопки // Финальные кнопки
if s.Text != nil { if s.Text != nil {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← "+cancelCallback, "cancel"), Button("← "+cancelCallback, "cancel"),
bot.Button(confirmText, "confirm_create"), Button(confirmText, "confirm_create"),
)) ))
} else { } else {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← "+cancelCallback, "cancel"), Button("← "+cancelCallback, "cancel"),
)) ))
} }
@@ -592,7 +587,7 @@ func (s *AddCreative) createCreative(b *bot.Bot) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start login", bot.Keyboard()) b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start login", Keyboard())
return return
} }
@@ -692,7 +687,7 @@ https://t.me/+AbCdEfGhIjKlMn</i>
🔘 <b>Кнопок добавлено:</b> %d`, len(s.Buttons)) 🔘 <b>Кнопок добавлено:</b> %d`, len(s.Buttons))
} }
b.SendNew(successText, bot.Keyboard()) b.SendNew(successText, Keyboard())
// Возвращаемся на экран креативов // Возвращаемся на экран креативов
if s.BackState != nil { if s.BackState != nil {

View File

@@ -30,10 +30,10 @@ type AddProject struct {
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
buttons := [][]echotron.InlineKeyboardButton{ buttons := [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
} }
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(addProjectInstructionText, keyboard, mode) b.Render(addProjectInstructionText, keyboard, mode)
} }

View File

@@ -32,7 +32,7 @@ func (s *AddPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return return
} }
@@ -55,11 +55,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
} }
if s.ActivePicker == "" { if s.ActivePicker == "" {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button(projectLabel, "pick_project"), Button(projectLabel, "pick_project"),
)) ))
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button(creativeLabel, "pick_creative"), Button(creativeLabel, "pick_creative"),
)) ))
} }
@@ -74,7 +74,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get projects") log.Error().Err(err).Msg("Failed to get projects")
b.SendNew("❌ Не удалось загрузить проекты", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить проекты", Keyboard())
return return
} }
@@ -90,11 +90,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
var slots []echotron.InlineKeyboardButton var slots []echotron.InlineKeyboardButton
for _, project := range page.Items { 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 { if page.Pages >= 2 {
for len(slots) < addPurchaseProjectsPerPage { for len(slots) < addPurchaseProjectsPerPage {
slots = append(slots, bot.Button(" ", "empty")) slots = append(slots, Button(" ", "empty"))
} }
} }
for i := 0; i < len(slots); i += 2 { 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) { if i+1 < len(slots) {
row = append(row, slots[i+1]) row = append(row, slots[i+1])
} else { } else {
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
buttons = append(buttons, row) 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( buttons = append(buttons, Row(
bot.Button("← Назад", "back_to_selection"), Button("← Назад", "back_to_selection"),
)) ))
case "creative_list": case "creative_list":
if s.ProjectID == "" { if s.ProjectID == "" {
text += "Сначала выберите проект, чтобы увидеть список креативов.\n\n" text += "Сначала выберите проект, чтобы увидеть список креативов.\n\n"
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back_to_selection"), Button("← Назад", "back_to_selection"),
)) ))
break break
} }
@@ -139,7 +139,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get creatives") log.Error().Err(err).Msg("Failed to get creatives")
b.SendNew("❌ Не удалось загрузить креативы", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить креативы", Keyboard())
return return
} }
@@ -157,11 +157,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt strin
var slots []echotron.InlineKeyboardButton var slots []echotron.InlineKeyboardButton
for _, creative := range page.Items { 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 { if page.Pages >= 2 {
for len(slots) < addPurchaseCreativesPerPage { for len(slots) < addPurchaseCreativesPerPage {
slots = append(slots, bot.Button(" ", "empty")) slots = append(slots, Button(" ", "empty"))
} }
} }
for i := 0; i < len(slots); i += 2 { 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) { if i+1 < len(slots) {
row = append(row, slots[i+1]) row = append(row, slots[i+1])
} else { } else {
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
buttons = append(buttons, row) 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( buttons = append(buttons, Row(
bot.Button("← Назад", "back_to_selection"), Button("← Назад", "back_to_selection"),
)) ))
} }
if s.ActivePicker == "" { if s.ActivePicker == "" {
if s.ProjectID != "" && s.CreativeID != "" { if s.ProjectID != "" && s.CreativeID != "" {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Отмена", "back"), Button("← Отмена", "back"),
bot.Button("→ Далее", "next"), Button("→ Далее", "next"),
)) ))
} else { } else {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Отмена", "back"), Button("← Отмена", "back"),
)) ))
} }
} }
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }

View File

@@ -24,11 +24,10 @@ type CreativeDetails struct {
} }
func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
// JWT уже создан в Bot.Update(), просто берем из сессии
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", bot.Keyboard()) b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", Keyboard())
return 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) creative, err := b.Backend.GetCreative(context.Background(), jwt, s.WorkspaceID, s.CreativeID)
if err != nil { if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative") log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative")
b.SendNew("<b>❌ Не удалось загрузить креатив</b>", bot.Keyboard( b.SendNew("<b>❌ Не удалось загрузить креатив</b>", Keyboard(
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
)) ))
return return
} }
@@ -96,7 +95,7 @@ func (s *CreativeDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
<b>Пример:</b> <b>Пример:</b>
<i>Присоединяйтесь к нашему каналу! <i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена редактирования", "cancel_edit")), Row(Button("« Отмена редактирования", "cancel_edit")),
}) })
case data == "add_button": case data == "add_button":
@@ -111,11 +110,11 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
• Написать нам • Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{ <i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row( Row(
bot.Button("✓ Ссылка на канал", "button_type_invite"), Button("✓ Ссылка на канал", "button_type_invite"),
bot.Button("Своя ссылка", "button_type_custom"), Button("Своя ссылка", "button_type_custom"),
), ),
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
case data == "add_media": case data == "add_media":
@@ -125,7 +124,7 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
Отправьте <b>фото, видео или GIF</b> Отправьте <b>фото, видео или GIF</b>
<i>Это медиа будет отображаться в креативе вместе с текстом</i>`, [][]echotron.InlineKeyboardButton{ <i>Это медиа будет отображаться в креативе вместе с текстом</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_media")), Row(Button("« Отмена", "cancel_media")),
}) })
case data == "delete_media": case data == "delete_media":
@@ -161,11 +160,11 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
• Написать нам • Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{ <i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row( Row(
bot.Button("✓ Ссылка на канал", "button_type_invite"), Button("✓ Ссылка на канал", "button_type_invite"),
bot.Button("Своя ссылка", "button_type_custom"), Button("Своя ссылка", "button_type_custom"),
), ),
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
case data == "button_type_custom": case data == "button_type_custom":
@@ -179,11 +178,11 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
• Написать нам • Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{ <i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row( Row(
bot.Button("Ссылка на канал", "button_type_invite"), Button("Ссылка на канал", "button_type_invite"),
bot.Button("✓ Своя ссылка", "button_type_custom"), Button("✓ Своя ссылка", "button_type_custom"),
), ),
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
case data == "cancel_edit", data == "cancel_media": case data == "cancel_edit", data == "cancel_media":
@@ -298,7 +297,7 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
<i>Например:</i> <i>Например:</i>
<code>https://example.com</code>`, [][]echotron.InlineKeyboardButton{ <code>https://example.com</code>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
} else { } else {
s.Buttons = append(s.Buttons, InlineButton{ s.Buttons = append(s.Buttons, InlineButton{
@@ -329,7 +328,7 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
URL должен начинаться с <code>http://</code> или <code>https://</code> URL должен начинаться с <code>http://</code> или <code>https://</code>
Попробуйте ещё раз:`, [][]echotron.InlineKeyboardButton{ Попробуйте ещё раз:`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")), Row(Button("« Отмена", "cancel_add_button")),
}) })
return 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) s.ShowManagementPanel(b, confirmText, confirmCallback, "cancel", deleteButton)
} }
@@ -404,7 +403,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", bot.Keyboard()) b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", Keyboard())
return return
} }
@@ -427,8 +426,8 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
log.Error().Err(err).Msg("Failed to download creative media") log.Error().Err(err).Msg("Failed to download creative media")
// Показываем ошибку через панель управления // Показываем ошибку через панель управления
s.ShowControlPanel(b, "<b>❌ Не удалось загрузить медиа</b>\n\nПопробуйте удалить медиа и добавить заново, или нажмите \"Сохранить\" без изменения медиа.", [][]echotron.InlineKeyboardButton{ s.ShowControlPanel(b, "<b>❌ Не удалось загрузить медиа</b>\n\nПопробуйте удалить медиа и добавить заново, или нажмите \"Сохранить\" без изменения медиа.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
bot.Row(bot.Button("✓ Сохранить без медиа", "confirm_save")), Row(Button("✓ Сохранить без медиа", "confirm_save")),
}) })
return 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") log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to update creative")
// Показываем ошибку через панель управления // Показываем ошибку через панель управления
s.ShowControlPanel(b, "<b>❌ Не удалось сохранить изменения</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{ s.ShowControlPanel(b, "<b>❌ Не удалось сохранить изменения</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
bot.Row(bot.Button("✓ Попробовать снова", "confirm_save")), Row(Button("✓ Попробовать снова", "confirm_save")),
}) })
return return
} }
@@ -471,7 +470,7 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
// Возвращаемся назад // Возвращаемся назад
if s.BackState != nil { if s.BackState != nil {
b.SendNew("✅ Креатив успешно обновлен!", bot.Keyboard()) b.SendNew("✅ Креатив успешно обновлен!", Keyboard())
b.SetState(s.BackState, bot.EditMessage) b.SetState(s.BackState, bot.EditMessage)
} }
} }
@@ -481,7 +480,7 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", bot.Keyboard()) b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", Keyboard())
return return
} }
@@ -498,8 +497,8 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
// Показываем ошибку через панель управления // Показываем ошибку через панель управления
s.confirmingDelete = false // Сбрасываем флаг подтверждения s.confirmingDelete = false // Сбрасываем флаг подтверждения
s.ShowControlPanel(b, "<b>❌ Не удалось удалить креатив</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{ s.ShowControlPanel(b, "<b>❌ Не удалось удалить креатив</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
bot.Row(bot.Button("⌦ Попробовать снова", "delete_creative")), Row(Button("⌦ Попробовать снова", "delete_creative")),
}) })
return return
} }
@@ -508,7 +507,7 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
// Возвращаемся назад // Возвращаемся назад
if s.BackState != nil { if s.BackState != nil {
b.SendNew("✅ Креатив успешно удален!", bot.Keyboard()) b.SendNew("✅ Креатив успешно удален!", Keyboard())
b.SetState(s.BackState, bot.EditMessage) b.SetState(s.BackState, bot.EditMessage)
} }
} }

View File

@@ -365,17 +365,17 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
var buttons [][]echotron.InlineKeyboardButton var buttons [][]echotron.InlineKeyboardButton
// Блок редактирования контента // Блок редактирования контента
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("✎ Текст", "edit_text"), Button("✎ Текст", "edit_text"),
bot.Button(" Медиа", "add_media"), Button(" Медиа", "add_media"),
)) ))
// Добавление кнопки сразу под основным блоком // Добавление кнопки сразу под основным блоком
buttons = append(buttons, bot.Row(bot.Button(" Добавить кнопку", "add_button"))) buttons = append(buttons, Row(Button(" Добавить кнопку", "add_button")))
// Кнопка удаления медиа (если медиа добавлено) // Кнопка удаления медиа (если медиа добавлено)
if e.MediaFileID != "" { if e.MediaFileID != "" {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("⌫ Убрать медиа", "delete_media"), Button("⌫ Убрать медиа", "delete_media"),
)) ))
} }
@@ -383,7 +383,7 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
if len(e.Buttons) > 0 { if len(e.Buttons) > 0 {
var row []echotron.InlineKeyboardButton var row []echotron.InlineKeyboardButton
for i, btn := range e.Buttons { for i, btn := range e.Buttons {
row = append(row, bot.Button( row = append(row, Button(
fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text), fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text),
fmt.Sprintf("delete_button:%d", i), fmt.Sprintf("delete_button:%d", i),
)) ))
@@ -406,13 +406,13 @@ func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText
// Финальные кнопки // Финальные кнопки
if e.Text != nil { if e.Text != nil {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", cancelCallback), Button("← Назад", cancelCallback),
bot.Button(confirmButtonText, confirmCallback), Button(confirmButtonText, confirmCallback),
)) ))
} else { } else {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", cancelCallback), 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) { func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons [][]echotron.InlineKeyboardButton) {
if e.ControlPanelMessageID == nil { if e.ControlPanelMessageID == nil {
// Отправляем новую панель // Отправляем новую панель
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.SendNew(text, keyboard) b.SendNew(text, keyboard)
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную! // ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
controlPanelMsgID := b.LastMessageID controlPanelMsgID := b.LastMessageID
@@ -437,7 +437,7 @@ func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons
oldLastMessageID := b.LastMessageID oldLastMessageID := b.LastMessageID
b.LastMessageID = *e.ControlPanelMessageID b.LastMessageID = *e.ControlPanelMessageID
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Edit(text, keyboard) b.Edit(text, keyboard)
// Восстанавливаем оригинальный LastMessageID // Восстанавливаем оригинальный LastMessageID

View File

@@ -26,7 +26,7 @@ func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", Keyboard())
return return
} }
@@ -45,7 +45,7 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get creatives") log.Error().Err(err).Msg("Failed to get creatives")
b.SendNew("❌ Не удалось загрузить креативы", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить креативы", Keyboard())
return return
} }
@@ -87,13 +87,13 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
// Создаем слоты для креативов // Создаем слоты для креативов
var slots []echotron.InlineKeyboardButton var slots []echotron.InlineKeyboardButton
for _, creative := range page.Items { 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 слотов пустыми кнопками // Если страниц >= 2, заполняем до 6 слотов пустыми кнопками
if page.Pages >= 2 { if page.Pages >= 2 {
for len(slots) < creativesPerPage { 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]) row = append(row, slots[i+1])
} else { } else {
// Если нечетное количество, добавляем пустую кнопку // Если нечетное количество, добавляем пустую кнопку
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
buttons = append(buttons, row) 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{ if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage, CurrentPage: s.CurrentPage,
TotalPages: page.Pages, TotalPages: page.Pages,
MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(" Добавить", "add_creative")}, MiddleButtons: []echotron.InlineKeyboardButton{Button(" Добавить", "add_creative")},
}); navRow != nil { }); navRow != nil {
buttons = append(buttons, navRow) buttons = append(buttons, navRow)
} }
// Нижний ряд: Назад + Архив // Нижний ряд: Назад + Архив
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
bot.Button("≡ Архив", "archive"), Button("≡ Архив", "archive"),
)) ))
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }
@@ -157,8 +157,8 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
case data == "archive": case data == "archive":
// TODO: реализовать экран архива креативов // TODO: реализовать экран архива креативов
b.Edit("📦 Архив креативов\n\nЭта функция в разработке", bot.Keyboard( b.Edit("📦 Архив креативов\n\nЭта функция в разработке", Keyboard(
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
)) ))
case data == "add_creative": case data == "add_creative":

View File

@@ -175,20 +175,20 @@ func (s *DateTimePicker) renderCalendarTitle() string {
func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup { func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
var rows [][]echotron.InlineKeyboardButton var rows [][]echotron.InlineKeyboardButton
rows = append(rows, bot.Row( rows = append(rows, Row(
s.monthPrevButton(), s.monthPrevButton(),
bot.Button(monthName(s.month), dtpCallbackPrefix+"open_months"), Button(monthName(s.month), dtpCallbackPrefix+"open_months"),
bot.Button("→", dtpCallbackPrefix+"month_next"), Button("→", dtpCallbackPrefix+"month_next"),
)) ))
rows = append(rows, bot.Row( rows = append(rows, Row(
bot.Button("Пн", "empty"), Button("Пн", "empty"),
bot.Button("Вт", "empty"), Button("Вт", "empty"),
bot.Button("Ср", "empty"), Button("Ср", "empty"),
bot.Button("Чт", "empty"), Button("Чт", "empty"),
bot.Button("Пт", "empty"), Button("Пт", "empty"),
bot.Button("Сб", "empty"), Button("Сб", "empty"),
bot.Button("Вс", "empty"), Button("Вс", "empty"),
)) ))
firstOfMonth := time.Date(s.year, s.month, 1, 0, 0, 0, 0, mskLocation) 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 var row []echotron.InlineKeyboardButton
for i := 1; i < weekday; i++ { for i := 1; i < weekday; i++ {
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
for day := 1; day <= daysInMonth; day++ { for day := 1; day <= daysInMonth; day++ {
date := time.Date(s.year, s.month, day, 0, 0, 0, 0, mskLocation) date := time.Date(s.year, s.month, day, 0, 0, 0, 0, mskLocation)
if date.Before(todayDate) { if date.Before(todayDate) {
row = append(row, bot.Button("-", "empty")) row = append(row, Button("-", "empty"))
} else { } else {
label := fmt.Sprintf("%d", day) 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 { if len(row) == 7 {
rows = append(rows, row) rows = append(rows, row)
@@ -221,17 +221,17 @@ func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
if len(row) > 0 { if len(row) > 0 {
for len(row) < 7 { for len(row) < 7 {
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
rows = append(rows, row) rows = append(rows, row)
} }
rows = append(rows, bot.Row( rows = append(rows, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
bot.Button("Отмена", "cancel"), Button("Отмена", "cancel"),
)) ))
return bot.Keyboard(rows...) return Keyboard(rows...)
} }
func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup { func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup {
@@ -239,25 +239,25 @@ func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup {
months := []string{"Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"} months := []string{"Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"}
for i := 0; i < 12; i += 3 { for i := 0; i < 12; i += 3 {
rows = append(rows, bot.Row( rows = append(rows, Row(
bot.Button(months[i], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+1)), Button(months[i], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+1)),
bot.Button(months[i+1], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+2)), Button(months[i+1], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+2)),
bot.Button(months[i+2], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+3)), Button(months[i+2], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+3)),
)) ))
} }
rows = append(rows, bot.Row( rows = append(rows, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
)) ))
return bot.Keyboard(rows...) return Keyboard(rows...)
} }
func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup { func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup {
var rows [][]echotron.InlineKeyboardButton 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 { for i := 0; i < 24; i += 6 {
rows = append(rows, bot.Row( rows = append(rows, Row(
s.hourButton(i), s.hourButton(i),
s.hourButton(i+1), s.hourButton(i+1),
s.hourButton(i+2), s.hourButton(i+2),
@@ -266,10 +266,10 @@ func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup {
s.hourButton(i+5), 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} minutes := []int{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}
for i := 0; i < len(minutes); i += 6 { 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]),
s.minuteButton(minutes[i+1]), s.minuteButton(minutes[i+1]),
s.minuteButton(minutes[i+2]), s.minuteButton(minutes[i+2]),
@@ -278,18 +278,18 @@ func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup {
s.minuteButton(minutes[i+5]), s.minuteButton(minutes[i+5]),
)) ))
} }
rows = append(rows, bot.Row( rows = append(rows, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
bot.Button("Готово", dtpCallbackPrefix+"confirm"), Button("Готово", dtpCallbackPrefix+"confirm"),
)) ))
return bot.Keyboard(rows...) return Keyboard(rows...)
} }
func (s *DateTimePicker) confirmKeyboard() echotron.InlineKeyboardMarkup { func (s *DateTimePicker) confirmKeyboard() echotron.InlineKeyboardMarkup {
return bot.Keyboard( return Keyboard(
bot.Row( Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
bot.Button("Готово", dtpCallbackPrefix+"confirm"), Button("Готово", dtpCallbackPrefix+"confirm"),
), ),
) )
} }
@@ -426,23 +426,23 @@ func (s *DateTimePicker) confirmSelection(b *bot.Bot) {
func (s *DateTimePicker) hourButton(hour int) echotron.InlineKeyboardButton { func (s *DateTimePicker) hourButton(hour int) echotron.InlineKeyboardButton {
label := fmt.Sprintf("%02d", hour) label := fmt.Sprintf("%02d", hour)
if s.isHourDisabled(hour) { if s.isHourDisabled(hour) {
return bot.Button("-", "empty") return Button("-", "empty")
} }
if s.selectedHour != nil && *s.selectedHour == hour { if s.selectedHour != nil && *s.selectedHour == hour {
label = "● " + label 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 { func (s *DateTimePicker) minuteButton(minute int) echotron.InlineKeyboardButton {
label := fmt.Sprintf("%02d", minute) label := fmt.Sprintf("%02d", minute)
if s.isMinuteDisabled(minute) { if s.isMinuteDisabled(minute) {
return bot.Button("-", "empty") return Button("-", "empty")
} }
if s.selectedMinute != nil && *s.selectedMinute == minute { if s.selectedMinute != nil && *s.selectedMinute == minute {
label = "● " + label 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 { func (s *DateTimePicker) isHourDisabled(hour int) bool {
@@ -499,9 +499,9 @@ func (s *DateTimePicker) nextMonth() {
func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton { func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton {
now := time.Now().In(mskLocation) now := time.Now().In(mskLocation)
if s.year == now.Year() && s.month == now.Month() { 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 { func daysInMonth(year int, month time.Month) int {

View File

@@ -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,
}
}

View File

@@ -21,14 +21,14 @@ Smartpost — платформа для покупки и продажи рек
` `
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
keyboard := bot.Keyboard( keyboard := Keyboard(
bot.Row( Row(
bot.Button("Мои проекты", "my_projects"), Button("Мои проекты", "my_projects"),
bot.URLButton("Поддержка", "https://example.com"), URLButton("Поддержка", "https://example.com"),
), ),
bot.Row( Row(
bot.Button("Закупы", "purchases"), Button("Закупы", "purchases"),
bot.URLButton("Дашборд", "https://example.com"), URLButton("Дашборд", "https://example.com"),
), ),
) )

View File

@@ -25,7 +25,7 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start login", Keyboard())
return return
} }
@@ -34,7 +34,7 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt) workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get workspaces") log.Error().Err(err).Msg("Failed to get workspaces")
b.SendNew("❌ Не удалось загрузить workspace'ы", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить workspace'ы", Keyboard())
return return
} }
@@ -57,7 +57,7 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
workspace, err := b.Backend.CreateWorkspace(context.Background(), jwt, username) workspace, err := b.Backend.CreateWorkspace(context.Background(), jwt, username)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to create workspace") log.Error().Err(err).Msg("Failed to create workspace")
b.SendNew("❌ Не удалось создать workspace", bot.Keyboard()) b.SendNew("❌ Не удалось создать workspace", Keyboard())
return 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) page, err := b.Backend.GetProjects(context.Background(), jwt, s.WorkspaceID, s.CurrentPage+1, projectsPerPage)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get projects") log.Error().Err(err).Msg("Failed to get projects")
b.SendNew("❌ Не удалось загрузить проекты", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить проекты", Keyboard())
return return
} }
@@ -122,13 +122,13 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
// Создаем слоты для проектов // Создаем слоты для проектов
var slots []echotron.InlineKeyboardButton var slots []echotron.InlineKeyboardButton
for _, project := range page.Items { 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 слотов пустыми кнопками // Если страниц >= 2, заполняем до 6 слотов пустыми кнопками
if page.Pages >= 2 { if page.Pages >= 2 {
for len(slots) < projectsPerPage { 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]) row = append(row, slots[i+1])
} else { } else {
// Если нечетное количество, добавляем пустую кнопку // Если нечетное количество, добавляем пустую кнопку
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
buttons = append(buttons, row) 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{ if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage, CurrentPage: s.CurrentPage,
TotalPages: page.Pages, TotalPages: page.Pages,
MiddleButtons: []echotron.InlineKeyboardButton{bot.Button(" Добавить", "add_project")}, MiddleButtons: []echotron.InlineKeyboardButton{Button(" Добавить", "add_project")},
}); navRow != nil { }); navRow != nil {
buttons = append(buttons, navRow) buttons = append(buttons, navRow)
} }
// Нижний ряд: Назад + Архив // Нижний ряд: Назад + Архив
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "menu"), Button("← Назад", "menu"),
bot.Button("≡ Архив", "archive"), Button("≡ Архив", "archive"),
)) ))
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }
@@ -189,8 +189,8 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(&MainMenu{}, bot.EditMessage) b.SetState(&MainMenu{}, bot.EditMessage)
case data == "archive": case data == "archive":
b.Edit("📦 Архив проектов\n\nЭта функция в разработке", bot.Keyboard( b.Edit("📦 Архив проектов\n\nЭта функция в разработке", Keyboard(
bot.Row(bot.Button("← Назад", "back_to_projects")), Row(Button("← Назад", "back_to_projects")),
)) ))
case data == "add_project": case data == "add_project":

View File

@@ -20,8 +20,8 @@ type ProjectDetails struct {
func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
if s.Project == nil { if s.Project == nil {
b.SendNew("❌ Проект не найден", bot.Keyboard( b.SendNew("❌ Проект не найден", Keyboard(
bot.Row(bot.Button("← Назад", "back_to_projects")), Row(Button("← Назад", "back_to_projects")),
)) ))
return return
} }
@@ -76,9 +76,9 @@ func (s *ProjectDetails) buildKeyboard() echotron.InlineKeyboardMarkup {
var buttons [][]echotron.InlineKeyboardButton var buttons [][]echotron.InlineKeyboardButton
// Первый ряд: Креативы и Закупы // Первый ряд: Креативы и Закупы
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("Креативы", fmt.Sprintf("creatives:%s", s.Project.ID)), Button("Креативы", fmt.Sprintf("creatives:%s", s.Project.ID)),
bot.Button("Закупы", fmt.Sprintf("purchases:%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) linkTypeButtonText = fmt.Sprintf("Вступление: %s", currentType)
} }
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)), Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)),
)) ))
// Нижний ряд: Назад и Архивировать // Нижний ряд: Назад и Архивировать
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back_to_projects"), Button("← Назад", "back_to_projects"),
bot.Button("≡ Архивировать", fmt.Sprintf("archive:%s", s.Project.ID)), 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) { 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 { if err != nil {
b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), bot.Keyboard( b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), Keyboard(
bot.Row(bot.Button("← Назад", "back_to_project")), Row(Button("← Назад", "back_to_project")),
)) ))
return return
} }
@@ -195,8 +195,8 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
default: default:
// Для остальных кнопок показываем заглушку // Для остальных кнопок показываем заглушку
b.Edit(fmt.Sprintf("🚧 Функция в разработке\n\nCallback: %s", data), bot.Keyboard( b.Edit(fmt.Sprintf("🚧 Функция в разработке\n\nCallback: %s", data), Keyboard(
bot.Row(bot.Button("← Назад", "back_to_project")), Row(Button("← Назад", "back_to_project")),
)) ))
} }
return return

View File

@@ -23,7 +23,7 @@ func (s *PurchaseDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return return
} }
@@ -40,7 +40,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get purchase") log.Error().Err(err).Msg("Failed to get purchase")
b.SendNew("❌ Не удалось загрузить закуп", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить закуп", Keyboard())
return return
} }
@@ -153,11 +153,11 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
var buttons [][]echotron.InlineKeyboardButton var buttons [][]echotron.InlineKeyboardButton
// Кнопка назад // Кнопка назад
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
)) ))
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }

View File

@@ -47,6 +47,7 @@ type PurchaseOptionalDetails struct {
CostCopy *CostEntry CostCopy *CostEntry
CostBeforeCopy *float64 CostBeforeCopy *float64
FormatCopy *string FormatCopy *string
ChannelEditMode string // "edit" или "copy"
BackState bot.State BackState bot.State
} }
@@ -58,6 +59,10 @@ type CostEntry struct {
func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
s.ensureDefaults() s.ensureDefaults()
if s.InputMode != "" { if s.InputMode != "" {
if s.InputMode == "mode_select" {
s.renderModeSelect(b, mode)
return
}
if s.renderParamScreens(b, mode) { if s.renderParamScreens(b, mode) {
return return
} }
@@ -72,30 +77,36 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
text += s.formatOptionalSummary() text += s.formatOptionalSummary()
var rows [][]echotron.InlineKeyboardButton var rows [][]echotron.InlineKeyboardButton
rows = append(rows, bot.Row( rows = append(rows, Row(
s.placementButton(), s.placementButton(),
s.costButton(), s.costButton(),
)) ))
rows = append(rows, bot.Row( rows = append(rows, Row(
s.paymentButton(), s.paymentButton(),
s.typeButton(), s.typeButton(),
)) ))
rows = append(rows, bot.Row( rows = append(rows, Row(
s.costBeforeButton(), s.costBeforeButton(),
s.formatButton(), s.formatButton(),
)) ))
rows = append(rows, bot.Row( rows = append(rows, Row(
s.commentButton(), s.commentButton(),
)) ))
// Кнопка переключения режима (только если несколько каналов)
if len(s.Channels) > 1 {
rows = append(rows, Row(s.globalModeButton()))
}
if row := s.costBeforeDeleteRow(); row != nil { if row := s.costBeforeDeleteRow(); row != nil {
rows = append(rows, row) rows = append(rows, row)
} }
if row := s.commentDeleteRow(); row != nil { if row := s.commentDeleteRow(); row != nil {
rows = append(rows, row) 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) b.Render(text, keyboard, mode)
} }
@@ -108,8 +119,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
switch u.CallbackQuery.Data { switch u.CallbackQuery.Data {
case "opt_datetime": 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) s.Enter(b, bot.EditMessage)
} else { } else {
s.setParamMode("placement", "common") s.setParamMode("placement", "common")
@@ -126,8 +138,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
}), bot.EditMessage) }), bot.EditMessage)
case "opt_cost": 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) s.Enter(b, bot.EditMessage)
} else { } else {
s.setParamMode("cost", "common") s.setParamMode("cost", "common")
@@ -139,8 +152,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "opt_format": 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) s.Enter(b, bot.EditMessage)
} else { } else {
s.setParamMode("format", "common") s.setParamMode("format", "common")
@@ -168,8 +182,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "cost_before": 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) s.Enter(b, bot.EditMessage)
} else { } else {
s.setParamMode("cost_before", "common") s.setParamMode("cost_before", "common")
@@ -210,7 +225,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return return
} }
s.createPurchase(b, jwt) s.createPurchase(b, jwt)
@@ -219,9 +234,57 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
b.SetState(s.BackState, bot.EditMessage) b.SetState(s.BackState, bot.EditMessage)
} }
default: default:
if u.CallbackQuery.Data == "noop" {
// Пустая кнопка - ничего не делаем
return
}
if s.handleParamCallback(b, u.CallbackQuery.Data) { if s.handleParamCallback(b, u.CallbackQuery.Data) {
return 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:") { if strings.HasPrefix(u.CallbackQuery.Data, "format:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "format:") value := strings.TrimPrefix(u.CallbackQuery.Data, "format:")
s.setFormatValue(value) s.setFormatValue(value)
@@ -251,7 +314,7 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
} }
switch s.InputMode { switch s.InputMode {
case "type": case "type_custom":
s.PurchaseType = text s.PurchaseType = text
case "format_custom": case "format_custom":
s.setFormatValue(text) s.setFormatValue(text)
@@ -350,87 +413,104 @@ func (s *PurchaseOptionalDetails) costTypeLabel() string {
} }
func (s *PurchaseOptionalDetails) placementButton() echotron.InlineKeyboardButton { func (s *PurchaseOptionalDetails) placementButton() echotron.InlineKeyboardButton {
if !s.hasPlacementValue() { icon := ""
return bot.Button(" Дата размещения", "opt_datetime") 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 { func (s *PurchaseOptionalDetails) paymentButton() echotron.InlineKeyboardButton {
if s.PaymentDate == nil { 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 { func (s *PurchaseOptionalDetails) costButton() echotron.InlineKeyboardButton {
if !s.hasCostValue() { icon := ""
return bot.Button(" Стоимость", "opt_cost") 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 { func (s *PurchaseOptionalDetails) costBeforeButton() echotron.InlineKeyboardButton {
if !s.hasCostBeforeValue() { icon := ""
return bot.Button(" До торга", "cost_before") 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 { func (s *PurchaseOptionalDetails) typeButton() echotron.InlineKeyboardButton {
if s.PurchaseType == "" { 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 { func (s *PurchaseOptionalDetails) formatButton() echotron.InlineKeyboardButton {
if !s.hasFormatValue() { icon := ""
return bot.Button(" Формат", "opt_format") 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 { func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton {
if s.Comment == "" { 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 { func (s *PurchaseOptionalDetails) commentDeleteRow() []echotron.InlineKeyboardButton {
if s.Comment == "" { if s.Comment == "" {
return nil return nil
} }
return bot.Row(bot.Button("⌫ Удалить комментарий", "delete_comment")) return Row(Button("⌫ Удалить комментарий", "delete_comment"))
} }
func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboardButton { func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboardButton {
if s.CostBeforeBargain == nil || s.CostBeforeMode == "per_channel" { if s.CostBeforeBargain == nil || s.CostBeforeMode == "per_channel" {
return nil 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) { func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderMode) {
text := "<i>… Создание Дополнительно</i>\n\n" text := "<i>… Создание Дополнительно</i>\n\n"
switch s.InputMode { switch s.InputMode {
case "type": case "type":
text += "<b>Введите тип закупа</b>\n\nНапример: <code>вп</code> или <code>стандарт</code>" text += "<b>Выберите тип закупа</b>"
case "cost_select": keyboard := Keyboard(
text = "<i>Страница ввода стоимости</i>\n\n" Row(
if s.CurrentChannel != "" { Button("Самопиар", "type:self_promo"),
text += fmt.Sprintf("Канал: <b>@%s</b>\n\n", s.CurrentChannel) Button("Стандарт", "type:standard"),
} ),
text += "Ставка СРМ / Фикс цена (по умолчанию фикс)\n\n" Row(Button("Другое", "type:custom")),
costValue := s.formatCostValue() Row(Button("← Назад", "back_to_optional")),
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())),
) )
if mode == bot.EditMessage { if mode == bot.EditMessage {
b.Edit(text, keyboard) b.Edit(text, keyboard)
@@ -438,11 +518,26 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
b.SendNew(text, keyboard) b.SendNew(text, keyboard)
} }
return return
case "type_custom":
text += "<b>Введите тип закупа</b>\n\nНапример: <code>вп</code>"
case "cost_value": case "cost_value":
text += "<b>Ввод стоимости (СРМ / фикс)</b>\n\nНапример: <code>15000</code>" text += "<b>Ввод стоимости</b>\n\nНапример: <code>15000</code>"
if s.CurrentChannel != "" { if s.CurrentChannel != "" {
text += fmt.Sprintf("\n\nКанал: <b>@%s</b>", s.CurrentChannel) text += fmt.Sprintf("\n\nКанал: <b>@%s</b>", 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": case "cost_before_value":
text += "<b>Ввод стоимости до торга</b>\n\nНапример: <code>20000</code>" text += "<b>Ввод стоимости до торга</b>\n\nНапример: <code>20000</code>"
if s.CurrentChannel != "" { if s.CurrentChannel != "" {
@@ -454,25 +549,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
text += fmt.Sprintf("Канал: <b>@%s</b>\n\n", s.CurrentChannel) text += fmt.Sprintf("Канал: <b>@%s</b>\n\n", s.CurrentChannel)
} }
text += "Выберите формат\n\n" text += "Выберите формат\n\n"
keyboard := bot.Keyboard( keyboard := Keyboard(
bot.Row( Row(
bot.Button("1 / 24", "format:1 / 24"), Button("1 / 24", "format:1 / 24"),
bot.Button("1/48", "format:1/48"), Button("1/48", "format:1/48"),
bot.Button("1 / 72", "format:1 / 72"), Button("1 / 72", "format:1 / 72"),
), ),
bot.Row( Row(
bot.Button("1 / (7 дней)", "format:1 / (7 дней)"), Button("1 / (7 дней)", "format:1 / (7 дней)"),
bot.Button("1 / (30 дней)", "format:1 / (30 дней)"), Button("1 / (30 дней)", "format:1 / (30 дней)"),
bot.Button("1 / (без удаления)", "format:1 / (без удаления)"), Button("1 / (без удаления)", "format:1 / (без удаления)"),
), ),
bot.Row( Row(
bot.Button("2 / 24", "format:2 / 24"), Button("2 / 24", "format:2 / 24"),
bot.Button("2/48", "format:2/48"), Button("2/48", "format:2/48"),
bot.Button("2/72", "format:2/72"), Button("2/72", "format:2/72"),
), ),
bot.Row( Row(
bot.Button("Назад", s.backAction()), Button("Назад", s.backAction()),
bot.Button("Свой формат", "format_custom"), Button("Свой формат", "format_custom"),
), ),
) )
if mode == bot.EditMessage { if mode == bot.EditMessage {
@@ -491,8 +586,8 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
return return
} }
keyboard := bot.Keyboard( keyboard := Keyboard(
bot.Row(bot.Button("← Назад", s.backAction())), Row(Button("← Назад", s.backAction())),
) )
if mode == bot.EditMessage { 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 := "<i>… Создание Дополнительно Выбор режима</i>\n\n"
text += "<b>⚙ Переключить режим параметра</b>\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 { func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.RenderMode) bool {
switch s.InputMode { switch s.InputMode {
case "placement_scope":
s.renderParamScope(b, mode, "placement")
return true
case "placement_channels": case "placement_channels":
s.renderParamChannels(b, mode, "placement") s.renderParamChannels(b, mode, "placement")
return true return true
case "cost_scope":
s.renderParamScope(b, mode, "cost")
return true
case "cost_channels": case "cost_channels":
s.renderParamChannels(b, mode, "cost") s.renderParamChannels(b, mode, "cost")
return true return true
case "cost_before_scope":
s.renderParamScope(b, mode, "cost_before")
return true
case "cost_before_channels": case "cost_before_channels":
s.renderParamChannels(b, mode, "cost_before") s.renderParamChannels(b, mode, "cost_before")
return true return true
case "format_scope":
s.renderParamScope(b, mode, "format")
return true
case "format_channels": case "format_channels":
s.renderParamChannels(b, mode, "format") s.renderParamChannels(b, mode, "format")
return true 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 := "<i>… Создание Дополнительно</i>\n\n"
text += fmt.Sprintf("<b>%s</b>\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) { func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.RenderMode, param string) {
s.CurrentParam = param s.CurrentParam = param
text := "<i>… Создание Дополнительно</i>\n\n" text := "<i>… Создание Дополнительно</i>\n\n"
@@ -568,7 +679,7 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende
total := len(s.Channels) total := len(s.Channels)
if total == 0 { if total == 0 {
text += "\nКаналы не выбраны" 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 return
} }
@@ -577,16 +688,74 @@ func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.Rende
} }
start, end := ui.GetPageBounds(s.ParamPage, perPage, total) start, end := ui.GetPageBounds(s.ParamPage, perPage, total)
// Устанавливаем режим по умолчанию
if s.ChannelEditMode == "" {
s.ChannelEditMode = "edit"
}
var rows [][]echotron.InlineKeyboardButton 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++ { for i := start; i < end; i++ {
label := fmt.Sprintf("%d", i+1) label := fmt.Sprintf("%d", i+1)
rows = append(rows, bot.Row( channelUsername := s.Channels[i].Username
bot.Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)),
bot.Button("⧉", fmt.Sprintf("param_copy:%s:%d", param, i)), if s.ChannelEditMode == "copy" {
bot.Button("⇲", fmt.Sprintf("param_paste:%s:%d", param, i)), // Режим копирования
bot.Button("Изменить", fmt.Sprintf("param_edit:%s:%d", param, i)), var channelRow []echotron.InlineKeyboardButton
bot.Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)), 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) 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, navRow)
} }
rows = append(rows, bot.Row(bot.Button("Применить ко всем", fmt.Sprintf("param_apply_all:%s", param)))) rows = append(rows, Row(
rows = append(rows, bot.Row( Button("← Назад", fmt.Sprintf("param_back:%s", param)),
bot.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 { 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 { func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) bool {
switch { 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:"): case strings.HasPrefix(data, "param_edit:"):
param, index, ok := s.parseParamIndex(data, "param_edit:") param, index, ok := s.parseParamIndex(data, "param_edit:")
if !ok { if !ok {
@@ -686,11 +840,6 @@ func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) b
} }
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return true 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"): case data == "prev" && strings.HasSuffix(s.InputMode, "_channels"):
if s.ParamPage > 0 { if s.ParamPage > 0 {
s.ParamPage-- s.ParamPage--
@@ -702,8 +851,8 @@ func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) b
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return true return true
case strings.HasPrefix(data, "param_back:"): case strings.HasPrefix(data, "param_back:"):
param := strings.TrimPrefix(data, "param_back:") // Возвращаемся на главный экран
s.InputMode = param + "_scope" s.InputMode = ""
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return true return true
} }
@@ -734,7 +883,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
BackState: s, BackState: s,
}), bot.EditMessage) }), bot.EditMessage)
case "cost": case "cost":
s.InputMode = "cost_select" s.InputMode = "cost_value"
s.ReturnMode = "" s.ReturnMode = ""
s.CurrentChannel = "" s.CurrentChannel = ""
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
@@ -766,7 +915,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, username
BackState: s, BackState: s,
}), bot.EditMessage) }), bot.EditMessage)
case "cost": case "cost":
s.InputMode = "cost_select" s.InputMode = "cost_value"
s.ReturnMode = "cost_channels" s.ReturnMode = "cost_channels"
s.CurrentChannel = username s.CurrentChannel = username
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
@@ -1084,6 +1233,40 @@ func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(username string) st
return fmt.Sprintf("%.0f₽", *value) 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 { func (s *PurchaseOptionalDetails) hasPlacementValue() bool {
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 { if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
for _, value := range s.PlacementByChannel { for _, value := range s.PlacementByChannel {
@@ -1209,16 +1392,16 @@ func (s *PurchaseOptionalDetails) backAction() string {
func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
if len(s.Channels) == 0 { if len(s.Channels) == 0 {
b.SendNew("❌ Добавьте хотя бы один канал", bot.Keyboard( b.SendNew("❌ Добавьте хотя бы один канал", Keyboard(
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
)) ))
return return
} }
purchaseType, ok := s.normalizePurchaseType() purchaseType, ok := s.normalizePurchaseType()
if !ok { if !ok {
b.SendNew("❌ Тип закупа: используйте «самопиар» или «стандарт»", bot.Keyboard( b.SendNew("❌ Тип закупа: используйте «самопиар» или «стандарт»", Keyboard(
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
)) ))
return return
} }
@@ -1256,8 +1439,8 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to create purchase") log.Error().Err(err).Msg("Failed to create purchase")
b.SendNew("❌ Не удалось создать закуп", bot.Keyboard( b.SendNew("❌ Не удалось создать закуп", Keyboard(
bot.Row(bot.Button("← Назад", "back")), Row(Button("← Назад", "back")),
)) ))
return return
} }

View File

@@ -20,7 +20,7 @@ func (s *Purchases) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return return
} }
@@ -36,7 +36,7 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get purchases") log.Error().Err(err).Msg("Failed to get purchases")
b.SendNew("❌ Не удалось загрузить закупы", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить закупы", Keyboard())
return return
} }
@@ -89,23 +89,23 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
totalChannels, totalChannels,
) )
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button(buttonText, fmt.Sprintf("purchase:%s", purchase.ID)), Button(buttonText, fmt.Sprintf("purchase:%s", purchase.ID)),
)) ))
} }
} }
// Кнопка создания нового закупа // Кнопка создания нового закупа
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button(" Создать закуп", "add_purchase"), Button(" Создать закуп", "add_purchase"),
)) ))
// Кнопка назад // Кнопка назад
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
)) ))
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }

View File

@@ -76,12 +76,12 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
if ch.PlannedCost != nil { if ch.PlannedCost != nil {
buttonText = fmt.Sprintf("@%s — %.0f₽", ch.Username, *ch.PlannedCost) 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 { if len(s.Channels) > channelsPerPage {
for len(slots) < 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) { if i+1 < len(slots) {
row = append(row, slots[i+1]) row = append(row, slots[i+1])
} else { } else {
row = append(row, bot.Button(" ", "empty")) row = append(row, Button(" ", "empty"))
} }
buttons = append(buttons, row) buttons = append(buttons, row)
} }
@@ -116,17 +116,17 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
// Кнопки навигации // Кнопки навигации
if len(s.Channels) > 0 { if len(s.Channels) > 0 {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
bot.Button("→ Далее", "next_step"), Button("→ Далее", "next_step"),
)) ))
} else { } else {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button("← Назад", "back"), Button("← Назад", "back"),
)) ))
} }
keyboard := bot.Keyboard(buttons...) keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }
@@ -152,8 +152,8 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
case "next_step": case "next_step":
if len(s.Channels) == 0 { if len(s.Channels) == 0 {
b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard( b.Edit("❌ Добавьте хотя бы один канал", Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")), Row(Button("← Назад", "back_to_select")),
)) ))
return return
} }
@@ -239,8 +239,8 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to create purchase") log.Error().Err(err).Msg("Failed to create purchase")
b.Edit("❌ Не удалось создать закуп\n\nВозможные причины:\n• Один из каналов не найден\n• Ошибка сервера", bot.Keyboard( b.Edit("❌ Не удалось создать закуп\n\nВозможные причины:\n• Один из каналов не найден\n• Ошибка сервера", Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")), Row(Button("← Назад", "back_to_select")),
)) ))
return return
} }

View File

@@ -20,20 +20,20 @@ func (s *SelectWorkspace) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT jwt := b.Session.JWT
if jwt == "" { if jwt == "" {
log.Error().Msg("JWT is empty in session") log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard()) b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return return
} }
workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt) workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get workspaces") log.Error().Err(err).Msg("Failed to get workspaces")
b.SendNew("❌ Не удалось загрузить список workspace'ов", bot.Keyboard()) b.SendNew("❌ Не удалось загрузить список workspace'ов", Keyboard())
return return
} }
if len(workspaces) == 0 { if len(workspaces) == 0 {
b.SendNew("У вас пока нет workspace'ов", bot.Keyboard( b.SendNew("У вас пока нет workspace'ов", Keyboard(
bot.Row(bot.Button("🏠 Главное меню", "back")), Row(Button("🏠 Главное меню", "back")),
)) ))
return return
} }
@@ -45,15 +45,15 @@ func (s *SelectWorkspace) Enter(b *bot.Bot, mode bot.RenderMode) {
// Создаем кнопки для каждого workspace // Создаем кнопки для каждого workspace
for _, ws := range workspaces { for _, ws := range workspaces {
buttons = append(buttons, bot.Row( buttons = append(buttons, Row(
bot.Button(ws.Name, fmt.Sprintf("select_workspace:%s", ws.ID)), 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) b.Render(text, keyboard, mode)
} }
@@ -81,7 +81,7 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
workspaceID := parts[1] workspaceID := parts[1]
// Показываем индикатор загрузки // Показываем индикатор загрузки
b.Edit("⏳ Добавляем канал в workspace...", bot.Keyboard()) b.Edit("⏳ Добавляем канал в workspace...", Keyboard())
// Вызываем API для привязки канала к workspace // Вызываем API для привязки канала к workspace
project, err := b.Backend.AttachChannelToWorkspace( project, err := b.Backend.AttachChannelToWorkspace(
@@ -92,8 +92,8 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
) )
if err != nil { if err != nil {
log.Error().Err(err).Str("channel_id", s.ChannelID).Str("workspace_id", workspaceID).Msg("Failed to attach channel to workspace") 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( b.Edit("❌ Не удалось добавить канал в workspace", Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")), Row(Button("← Назад", "back_to_select")),
)) ))
return return
} }
@@ -108,9 +108,9 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
project.Status, project.Status,
) )
b.Edit(successText, bot.Keyboard( b.Edit(successText, Keyboard(
bot.Row(bot.Button("📋 Мои проекты", "my_projects")), Row(Button("📋 Мои проекты", "my_projects")),
bot.Row(bot.Button("🏠 Главное меню", "main_menu")), Row(Button("🏠 Главное меню", "main_menu")),
)) ))
case data == "back_to_select": case data == "back_to_select":

View File

@@ -3,7 +3,6 @@ package ui
import ( import (
"fmt" "fmt"
"example.com/m/bot"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
) )
@@ -42,7 +41,10 @@ func BuildNavigationRow(config PaginationConfig) []echotron.InlineKeyboardButton
// Левая стрелка (только если есть предыдущая страница) // Левая стрелка (только если есть предыдущая страница)
if totalPages > 1 && config.CurrentPage > 0 { 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 { 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 { if len(row) == 0 {