bot changes

This commit is contained in:
Artem Tsyrulnikov
2026-01-03 11:24:24 +03:00
parent 0577d2a00e
commit 922067d194
13 changed files with 312 additions and 182 deletions

View File

@@ -19,6 +19,11 @@ func Button(text, data string) echotron.InlineKeyboardButton {
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

@@ -20,14 +20,16 @@ type AddCreative struct {
}
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
text := `<b> Добавить креатив</b>
text := `<i>… Креативы Добавление</i>
<b> Добавить креатив</b>
Введите <b>название</b> креатива:
<i>Например:</i>
Летняя акция
Баннер 1920x1080
Видео промо`
Летняя акция
Баннер 1920x1080
Видео промо`
buttons := [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel")),
@@ -286,6 +288,9 @@ func (s *AddCreative) Handle(_ *bot.Bot, _ *echotron.Update) { return }
// sendCreativeAndPanel отправляет превью креатива и панель управления
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
// Устанавливаем навигацию
s.Navigation = "… Креативы Добавление"
// 1. Отправляем превью креатива
s.SendCreativePreview(b)

View File

@@ -14,6 +14,7 @@ const addPurchaseCreativesPerPage = 5
type AddPurchase struct {
WorkspaceID string
ProjectID string
ProjectTitle string
CurrentPage int
BackState bot.State
}
@@ -46,14 +47,14 @@ func (s *AddPurchase) renderCreativeSelection(b *bot.Bot, mode bot.RenderMode, j
return
}
text := "<b> Создание закупа</b>\n\n"
text := "<i>… Закупы Создание</i>\n\n<b>Создание закупа</b>\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text += `Для создания закупа сначала нужно создать креатив.
<i>Вернитесь назад и создайте креатив в разделе "Креативы"</i>`
Вернитесь назад и создайте креатив в разделе <b>Креативы</b>`
buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"),
@@ -61,12 +62,12 @@ func (s *AddPurchase) renderCreativeSelection(b *bot.Bot, mode bot.RenderMode, j
} else {
pageInfo := ""
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages)
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text += fmt.Sprintf(`<b>Шаг 1/2: Выбор креатива</b>%s
text += fmt.Sprintf(`<b>Шаг 1/2:</b> Выбор креатива%s
<i>Выберите креатив для закупа:</i>
Выберите креатив для закупа
`, pageInfo)
@@ -145,6 +146,7 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(&SelectChannelsForPurchase{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: creativeID,
Channels: []PurchaseChannelInput{},
BackState: s.BackState,

View File

@@ -52,6 +52,9 @@ func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
}
// Buttons, MediaType, MediaFileID остаются пустыми (не сохраняются в БД)
// Устанавливаем навигацию
s.Navigation = "… Креативы Детали"
log.Info().
Str("creative_id", s.CreativeID).
Str("name", creative.Name).

View File

@@ -2,6 +2,7 @@ package screens
import (
"fmt"
"sort"
"strings"
"example.com/m/bot"
@@ -26,6 +27,9 @@ type CreativeEditorFields struct {
MediaType string // "photo", "video", "animation"
MediaFileID string // File ID из Telegram
// Навигация (хлебные крошки)
Navigation string
// Состояние для добавления кнопки
WaitingForButtonText bool
WaitingForButtonURL bool
@@ -43,6 +47,13 @@ type CreativeEditorFields struct {
// GetCreativeText возвращает текст для превью креатива
func (e *CreativeEditorFields) GetCreativeText() string {
var result string
// Добавляем навигацию если она есть
if e.Navigation != "" {
result = fmt.Sprintf("<i>%s</i>\n\n", e.Navigation)
}
if e.Text != nil && *e.Text != "" {
// Проверяем что текст не является "визуально пустым" HTML
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
@@ -51,7 +62,7 @@ func (e *CreativeEditorFields) GetCreativeText() string {
// Убираем пустые HTML-теги (простая проверка)
if text == "<a class=\"tg-link\"></a>" || text == "" {
log.Info().Str("text", *e.Text).Msg("GetCreativeText: text is visually empty, using default")
return `<i>💭 Я твой креатив
return result + `<i>💭 Я твой креатив
У меня пока нет текста, добавь его ниже.
@@ -59,10 +70,10 @@ func (e *CreativeEditorFields) GetCreativeText() string {
}
log.Info().Str("text", *e.Text).Msg("GetCreativeText: returning creative text")
return *e.Text
return result + *e.Text
}
log.Info().Bool("text_is_nil", e.Text == nil).Msg("GetCreativeText: returning default text")
return `<i>💭 Я твой креатив
return result + `<i>💭 Я твой креатив
У меня пока нет текста, добавь его ниже.
@@ -361,54 +372,89 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
return message.Text
}
type tagSpan struct {
open string
close string
start int
end int
len int
}
// Преобразуем текст в руны для правильной работы с UTF-8
runes := []rune(message.Text)
result := ""
lastPos := 0
// Обрабатываем entities
var spans []tagSpan
for _, entity := range message.Entities {
// Добавляем текст до entity
if entity.Offset > lastPos {
result += string(runes[lastPos:entity.Offset])
// URL уже присутствует в сыром тексте, чтобы не дублировать - пропускаем entity.
if entity.Type == "url" {
continue
}
// Получаем текст entity
entityText := string(runes[entity.Offset : entity.Offset+entity.Length])
entityText := ""
if entity.Offset+entity.Length <= len(runes) {
entityText = string(runes[entity.Offset : entity.Offset+entity.Length])
}
// Оборачиваем в соответствующие HTML-теги
var openTag, closeTag string
switch entity.Type {
case "bold":
result += "<b>" + entityText + "</b>"
openTag, closeTag = "<b>", "</b>"
case "italic":
result += "<i>" + entityText + "</i>"
openTag, closeTag = "<i>", "</i>"
case "underline":
result += "<u>" + entityText + "</u>"
openTag, closeTag = "<u>", "</u>"
case "strikethrough":
result += "<s>" + entityText + "</s>"
openTag, closeTag = "<s>", "</s>"
case "code":
result += "<code>" + entityText + "</code>"
openTag, closeTag = "<code>", "</code>"
case "pre":
result += "<pre>" + entityText + "</pre>"
openTag, closeTag = "<pre>", "</pre>"
case "text_link":
if entity.URL != "" {
result += fmt.Sprintf("<a href=\"%s\">%s</a>", entity.URL, entityText)
} else {
result += entityText
openTag = fmt.Sprintf("<a href=\"%s\">", entity.URL)
closeTag = "</a>"
}
case "url":
result += entityText
default:
result += entityText
_ = entityText
}
lastPos = entity.Offset + entity.Length
if openTag != "" && closeTag != "" {
spans = append(spans, tagSpan{
open: openTag,
close: closeTag,
start: entity.Offset,
end: entity.Offset + entity.Length,
len: entity.Length,
})
}
}
// Добавляем оставшийся текст
if lastPos < len(runes) {
result += string(runes[lastPos:])
opens := make(map[int][]tagSpan)
closes := make(map[int][]tagSpan)
for _, span := range spans {
opens[span.start] = append(opens[span.start], span)
closes[span.end] = append(closes[span.end], span)
}
return result
// Пишем текст один раз, вставляя HTML-теги по позициям
var result strings.Builder
for i := 0; i <= len(runes); i++ {
if closing, ok := closes[i]; ok {
sort.Slice(closing, func(a, b int) bool { return closing[a].len < closing[b].len })
for _, span := range closing {
result.WriteString(span.close)
}
}
if opening, ok := opens[i]; ok {
sort.Slice(opening, func(a, b int) bool { return opening[a].len > opening[b].len })
for _, span := range opening {
result.WriteString(span.open)
}
}
if i < len(runes) {
result.WriteRune(runes[i])
}
}
return result.String()
}

View File

@@ -15,6 +15,7 @@ const creativesPerPage = 6
type Creatives struct {
CurrentPage int
ProjectID string
ProjectTitle string
WorkspaceID string
BackState bot.State
}
@@ -49,29 +50,40 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
var text string
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text = `<b>Креативы</b>
// Навигация: 2 последних уровня
navPath := "… Креативы"
if s.ProjectTitle != "" {
navPath = fmt.Sprintf("… %s Креативы", s.ProjectTitle)
}
У вас пока нет креативов.
if len(page.Items) == 0 {
text = fmt.Sprintf(`<i>%s</i>
<b>Креативы</b>
У вас пока нет креативов
<i>Креатив — это рекламный материал:</i>
Текст объявления
Медиа контент (фото, видео)
Шаблон для автопостинга
Варианты для A/B тестирования
Текст объявления
Медиа контент (фото, видео)
Шаблон для автопостинга
Варианты для A/B тестирования
<i>Начните с добавления первого креатива 👇</i>`
Начните с добавления первого креатива`, navPath)
} else {
pageInfo := ""
if page.Pages > 1 {
pageInfo = fmt.Sprintf("\n<i>Страница %d из %d</i>", page.Page, page.Pages)
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text = fmt.Sprintf(`<b>Креативы</b>
text = fmt.Sprintf(`<i>%s</i>
Управление рекламными материалами.%s
<b>Креативы</b>%s
<i>Выберите креатив 👇</i>`,
Управление рекламными материалами
Выберите креатив`,
navPath,
pageInfo)
}

View File

@@ -9,23 +9,27 @@ type MainMenu struct{}
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
keyboard := bot.Keyboard(
bot.Row(bot.Button("Мои проекты", "my_projects"), bot.Button("Поддержка", "support")),
bot.Row(bot.Button("Закупы", "placements"), bot.Button("Дашборд", "dashboard")),
bot.Row(
bot.Button("Мои проекты", "my_projects"),
echotron.InlineKeyboardButton{Text: "Поддержка", URL: "https://example.com"},
),
bot.Row(
bot.Button("Закупы", "purchases"),
echotron.InlineKeyboardButton{Text: "Дашборд", URL: "https://example.com"},
),
)
b.Render(tgexAbout, keyboard, mode)
text := "<i>Главное меню</i>\n\n" + tgexAbout
b.Render(text, keyboard, mode)
}
func (s *MainMenu) HandleCallback(b *bot.Bot, u *echotron.Update) {
switch u.CallbackQuery.Data {
case "my_projects":
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.NewMessage)
case "support":
// b.SetState(&Support{}, bot.NewMessage)
case "placements":
b.SetState(&MyPlacements{BackState: &MainMenu{}}, bot.NewMessage)
case "dashboard":
// b.SetState(&Dashboard{}, bot.NewMessage)
case "purchases":
b.SetState(&MyProjects{BackState: &MainMenu{}, OpenPurchases: true}, bot.NewMessage)
default:
s.Enter(b, bot.NewMessage)
}

View File

@@ -66,16 +66,20 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
return
}
text := "<b>📍 Мои размещения</b>\n\n"
text := "<i>… Главное меню Размещения</i>\n\n<b>Мои размещения</b>\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text += `У вас пока нет размещений.
text += `У вас пока нет размещений
<i>Размещения создаются автоматически при создании закупа.</i>`
Размещения создаются автоматически при создании закупа`
} else {
text += fmt.Sprintf("Выберите размещение для деталей (стр. %d из %d):\n\n", s.CurrentPage+1, page.Pages)
pageInfo := ""
if page.Pages > 1 {
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", s.CurrentPage+1, page.Pages)
}
text += fmt.Sprintf("Выберите размещение для деталей%s\n\n", pageInfo)
// Размещения по 2 в ряду
for i := 0; i < len(page.Items); i += 2 {
@@ -131,7 +135,7 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
// Кнопки управления
buttons = append(buttons, bot.Row(
bot.Button("📊 Статистика", "statistics"),
bot.Button(" Статистика", "statistics"),
))
// Кнопка возврата в главное меню

View File

@@ -16,6 +16,7 @@ type MyProjects struct {
CurrentPage int
WorkspaceID string
BackState bot.State
OpenPurchases bool
}
func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
@@ -58,32 +59,46 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
var text string
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text = `<b>Мои проекты</b>
У вас пока нет подключенных каналов.
navTitle := "Главное меню Мои проекты"
headerTitle := "Мои проекты"
emptyStateText := `У вас пока нет подключенных каналов.
<i>Что дает подключение канала:</i>
▸ Управление типом приглашений (публичные/с одобрением)
▸ Автоматическая отправка креативов в канал
▸ Интеграция с планом закупов
▸ Статистика и аналитика подписчиков
▸ Настройка уведомлений
▸ Управление типом приглашений (публичные/с одобрением)
▸ Автоматическая отправка креативов в канал
▸ Интеграция с планом закупов
▸ Статистика и аналитика подписчиков
▸ Настройка уведомлений
<i>Начните с добавления первого проекта ↓</i>`
Начните с добавления первого проекта`
nonEmptyText := "Управление вашими Telegram-каналами.\n\nВыберите канал"
if s.OpenPurchases {
navTitle = "Главное меню Закупы"
headerTitle = "Закупы"
nonEmptyText = "Выберите проект для просмотра закупов"
}
if len(page.Items) == 0 {
text = fmt.Sprintf(`<i>%s</i>
<b>%s</b>
%s`, navTitle, headerTitle, emptyStateText)
} else {
pageInfo := ""
if page.Pages > 1 {
pageInfo = fmt.Sprintf("\n<i>Страница %d из %d</i>", page.Page, page.Pages)
pageInfo = fmt.Sprintf(" <i>(стр. %d/%d)</i>", page.Page, page.Pages)
}
text = fmt.Sprintf(`<b>Мои проекты</b>
text = fmt.Sprintf(`<i>%s</i>
Управление вашими Telegram-каналами.%s
<b>%s</b>%s
<i>Выберите канал 👇 </i>`,
pageInfo)
%s`,
navTitle,
headerTitle,
pageInfo,
nonEmptyText)
}
// Создаем слоты для проектов
@@ -216,12 +231,21 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
return
}
if s.OpenPurchases {
b.SetState(&Purchases{
WorkspaceID: s.WorkspaceID,
ProjectID: project.ID,
ProjectTitle: project.Title,
BackState: &MyProjects{WorkspaceID: s.WorkspaceID, OpenPurchases: true},
}, bot.NewMessage)
} else {
b.SetState(&ProjectDetails{
Project: project,
WorkspaceID: s.WorkspaceID,
BackState: &MyProjects{WorkspaceID: s.WorkspaceID},
}, bot.NewMessage)
}
}
case strings.HasPrefix(data, "workspace:"):
// Позволяет переключаться между workspace'ами в будущем

View File

@@ -34,33 +34,40 @@ func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
func (s *ProjectDetails) formatProjectDetails() string {
p := s.Project
// Заголовок с названием проекта
text := fmt.Sprintf("<b>%s</b>\n\n", p.Title)
// Хлебные крошки и заголовок
text := fmt.Sprintf("<i>… Мои проекты %s</i>\n\n", p.Title)
text += fmt.Sprintf("<b>%s</b>\n\n", p.Title)
// Информация о канале
if p.Username != nil && *p.Username != "" {
text += fmt.Sprintf("Telegram-канал: @%s\n", *p.Username)
text += fmt.Sprintf("Telegram: @%s\n", *p.Username)
} else {
text += "Telegram-канал: <i>не привязан</i>\n"
text += "Telegram: <i>не привязан</i>\n"
}
// Статус проекта
var statusSymbol string
var statusText string
switch p.Status {
case "active":
statusText = "✓ Активный"
statusSymbol = ""
statusText = "Активный"
case "inactive":
statusText = "○ Неактивен"
statusSymbol = "○"
statusText = "Неактивен"
case "archived":
statusText = "▣ Архивный"
statusSymbol = ""
statusText = "Архивный"
case "paused":
statusText = "⊙ Приостановлен"
statusSymbol = ""
statusText = "Приостановлен"
default:
statusSymbol = "○"
statusText = p.Status
}
text += fmt.Sprintf("Статус: %s\n", statusText)
text += fmt.Sprintf("Статус: %s %s\n", statusSymbol, statusText)
text += "\n<i>Управление проектом 👇</i>"
text += "\nУправление проектом"
return text
}
@@ -126,6 +133,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
b.SetState(&Creatives{
ProjectID: s.Project.ID,
ProjectTitle: s.Project.Title,
WorkspaceID: s.WorkspaceID,
BackState: s,
}, bot.NewMessage)
@@ -135,6 +143,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(&Purchases{
WorkspaceID: s.WorkspaceID,
ProjectID: s.Project.ID,
ProjectTitle: s.Project.Title,
BackState: s,
}, bot.NewMessage)

View File

@@ -12,6 +12,7 @@ import (
type PurchaseDetails struct {
WorkspaceID string
ProjectID string
ProjectTitle string
PurchaseID string
BackState bot.State
}
@@ -53,21 +54,23 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
creative = nil
}
text := "<b>📦 Детали закупа</b>\n\n"
text := "<i>… Закупы Детали</i>\n\n<b>Детали закупа</b>\n\n"
// Информация о креативе
if creative != nil {
text += fmt.Sprintf("<b>Креатив:</b> %s\n", creative.Name)
text += fmt.Sprintf("<b>Креатив:</b> %s\n", creative.Name)
} else {
text += fmt.Sprintf("<b>Креатив ID:</b> %s\n", purchase.CreativeID)
text += fmt.Sprintf("<b>Креатив ID:</b> %s\n", purchase.CreativeID)
}
// Статус закупа
statusSymbol := "●"
statusText := "Активный"
if purchase.Status == "archived" {
statusSymbol = "■"
statusText = "Архивирован"
}
text += fmt.Sprintf("<b>Статус:</b> %s\n\n", statusText)
text += fmt.Sprintf("<b>Статус:</b> %s %s\n\n", statusSymbol, statusText)
// Информация о каналах
if len(purchase.Channels) == 0 {
@@ -76,19 +79,19 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
text += fmt.Sprintf("<b>Каналы (%d):</b>\n\n", len(purchase.Channels))
for i, ch := range purchase.Channels {
// Определяем статус emoji
statusEmoji := "📌"
// Определяем статус
statusSymbol := ""
switch ch.Status {
case "planned":
statusEmoji = "📝"
statusSymbol = ""
case "approved":
statusEmoji = ""
statusSymbol = ""
case "rejected":
statusEmoji = ""
statusSymbol = ""
case "in_progress":
statusEmoji = "🔄"
statusSymbol = ""
case "completed":
statusEmoji = "✔️"
statusSymbol = ""
}
// Название канала
@@ -99,16 +102,16 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
channelName = "@" + *ch.Channel.Username
}
text += fmt.Sprintf("%d. %s <b>%s</b>\n", i+1, statusEmoji, channelName)
text += fmt.Sprintf("%d. %s <b>%s</b>\n", i+1, statusSymbol, channelName)
// Стоимость
if ch.PlannedCost != nil {
text += fmt.Sprintf(" 💰 Стоимость: %.0f₽\n", *ch.PlannedCost)
text += fmt.Sprintf(" Стоимость: %.0f₽\n", *ch.PlannedCost)
}
// Комментарий
if ch.Comment != nil && *ch.Comment != "" {
text += fmt.Sprintf(" 💬 %s\n", *ch.Comment)
text += fmt.Sprintf(" %s\n", *ch.Comment)
}
// Тип invite link
@@ -116,7 +119,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
if ch.InviteLinkType == "approval" {
inviteLinkTypeText = "с одобрением"
}
text += fmt.Sprintf(" 🔗 Ссылка: %s\n", inviteLinkTypeText)
text += fmt.Sprintf(" Ссылка: %s\n", inviteLinkTypeText)
text += "\n"
}

View File

@@ -12,6 +12,7 @@ import (
type Purchases struct {
WorkspaceID string
ProjectID string
ProjectTitle string
BackState bot.State
}
@@ -39,7 +40,12 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
return
}
text := "<b>📦 Закупы</b>\n\n"
// Навигация: 2 последних уровня
navPath := "… Закупы"
if s.ProjectTitle != "" {
navPath = fmt.Sprintf("… %s Закупы", s.ProjectTitle)
}
text := fmt.Sprintf("<i>%s</i>\n\n<b>Закупы</b>\n\n", navPath)
var buttons [][]echotron.InlineKeyboardButton
@@ -47,24 +53,24 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
text += `Список закупов пуст.
<i>Закуп — это план размещения рекламы в каналах:</i>
Выбор креатива
Список каналов для размещения
Планируемая стоимость
Статус выполнения
Выбор креатива
Список каналов для размещения
Планируемая стоимость
Статус выполнения
<i>Создайте первый закуп 👇</i>`
Создайте первый закуп`
} else {
text += "Управление закупами для проекта.\n\n"
text += "<i>Выберите закуп для деталей:</i>\n\n"
text += "Выберите закуп для деталей:\n\n"
for i, purchase := range purchases {
// Определяем статус emoji
statusEmoji := "📌"
// Определяем статус
statusSymbol := ""
switch purchase.Status {
case "active":
statusEmoji = "🟢"
statusSymbol = ""
case "archived":
statusEmoji = "📦"
statusSymbol = ""
}
// Считаем каналы по статусам
@@ -77,7 +83,7 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
}
buttonText := fmt.Sprintf("%s Закуп #%d (%d/%d каналов)",
statusEmoji,
statusSymbol,
i+1,
completedChannels,
totalChannels,
@@ -134,6 +140,7 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(&AddPurchase{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
BackState: s,
}, bot.EditMessage)
@@ -144,6 +151,7 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
b.SetState(&PurchaseDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
PurchaseID: purchaseID,
BackState: s,
}, bot.NewMessage)

View File

@@ -20,6 +20,7 @@ type PurchaseChannelInput struct {
type SelectChannelsForPurchase struct {
WorkspaceID string
ProjectID string
ProjectTitle string
CreativeID string
Channels []PurchaseChannelInput
BackState bot.State
@@ -30,34 +31,35 @@ func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
}
func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.RenderMode) {
text := "<b> Создание закупа</b>\n\n"
text += "<b>Шаг 2/2: Добавление каналов</b>\n\n"
text := "<i>… Создание Каналы</i>\n\n"
text += "<b>Создание закупа</b>\n\n"
text += "<b>Шаг 2/2:</b> Добавление каналов\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(s.Channels) == 0 {
text += `Добавьте каналы для размещения рекламы.
text += `Добавьте каналы для размещения рекламы
<i>Отправьте username канала (без @)</i>
Отправьте username канала (без @)
Например: <code>channel_name</code>
После добавления всех каналов нажмите "Создать закуп"`
После добавления всех каналов нажмите <b>Создать закуп</b>`
} else {
text += fmt.Sprintf("<b>Добавлено каналов: %d</b>\n\n", len(s.Channels))
text += fmt.Sprintf("Добавлено каналов: <b>%d</b>\n\n", len(s.Channels))
for i, ch := range s.Channels {
channelText := fmt.Sprintf("%d. @%s", i+1, ch.Username)
channelText := fmt.Sprintf(" %d. @%s", i+1, ch.Username)
if ch.PlannedCost != nil {
channelText += fmt.Sprintf(" (%.0f₽)", *ch.PlannedCost)
channelText += fmt.Sprintf(" %.0f₽", *ch.PlannedCost)
}
text += channelText + "\n"
}
text += "\n<i>Добавьте еще каналы или создайте закуп</i>"
text += "\nДобавьте еще каналы или создайте закуп"
// Кнопка создания закупа (доступна только если есть каналы)
buttons = append(buttons, bot.Row(
bot.Button(" Создать закуп", "create"),
bot.Button(" Создать закуп", "create"),
))
}
@@ -97,6 +99,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
b.SetState(&AddPurchase{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
BackState: s.BackState,
}, bot.EditMessage)
@@ -159,16 +162,18 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
}
// Показываем успешное создание
successText := fmt.Sprintf(`✅ <b>Закуп создан!</b>
successText := fmt.Sprintf(`<i>… Создание Готово</i>
<b>ID:</b> %s
<b>Креатив:</b> %s
<b>Каналов:</b> %d
<b>Закуп создан</b>
Закуп создан и готов к работе.`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
▪ <b>ID:</b> %s
▪ <b>Креатив:</b> %s
▪ <b>Каналов:</b> %d
Закуп создан и готов к работе`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
b.Edit(successText, bot.Keyboard(
bot.Row(bot.Button(" Готово", "done")),
bot.Row(bot.Button(" Готово", "done")),
))
}