bot changes
This commit is contained in:
@@ -19,6 +19,11 @@ func Button(text, data string) echotron.InlineKeyboardButton {
|
|||||||
func Row(buttons ...echotron.InlineKeyboardButton) []echotron.InlineKeyboardButton { return buttons }
|
func Row(buttons ...echotron.InlineKeyboardButton) []echotron.InlineKeyboardButton { return buttons }
|
||||||
|
|
||||||
func Keyboard(rows ...[]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
|
func Keyboard(rows ...[]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return echotron.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]echotron.InlineKeyboardButton{},
|
||||||
|
}
|
||||||
|
}
|
||||||
return echotron.InlineKeyboardMarkup{
|
return echotron.InlineKeyboardMarkup{
|
||||||
InlineKeyboard: rows,
|
InlineKeyboard: rows,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ type AddCreative struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
text := `<b>+ Добавить креатив</b>
|
text := `<i>… › Креативы › Добавление</i>
|
||||||
|
|
||||||
|
<b>+ Добавить креатив</b>
|
||||||
|
|
||||||
Введите <b>название</b> креатива:
|
Введите <b>название</b> креатива:
|
||||||
|
|
||||||
<i>Например:</i>
|
<i>Например:</i>
|
||||||
• Летняя акция
|
‣ Летняя акция
|
||||||
• Баннер 1920x1080
|
‣ Баннер 1920x1080
|
||||||
• Видео промо`
|
‣ Видео промо`
|
||||||
|
|
||||||
buttons := [][]echotron.InlineKeyboardButton{
|
buttons := [][]echotron.InlineKeyboardButton{
|
||||||
bot.Row(bot.Button("« Отмена", "cancel")),
|
bot.Row(bot.Button("« Отмена", "cancel")),
|
||||||
@@ -286,6 +288,9 @@ func (s *AddCreative) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
|||||||
|
|
||||||
// sendCreativeAndPanel отправляет превью креатива и панель управления
|
// sendCreativeAndPanel отправляет превью креатива и панель управления
|
||||||
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
|
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
|
||||||
|
// Устанавливаем навигацию
|
||||||
|
s.Navigation = "… › Креативы › Добавление"
|
||||||
|
|
||||||
// 1. Отправляем превью креатива
|
// 1. Отправляем превью креатива
|
||||||
s.SendCreativePreview(b)
|
s.SendCreativePreview(b)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const addPurchaseCreativesPerPage = 5
|
|||||||
type AddPurchase struct {
|
type AddPurchase struct {
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
ProjectID string
|
ProjectID string
|
||||||
|
ProjectTitle string
|
||||||
CurrentPage int
|
CurrentPage int
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
@@ -46,14 +47,14 @@ func (s *AddPurchase) renderCreativeSelection(b *bot.Bot, mode bot.RenderMode, j
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text := "<b>➕ Создание закупа</b>\n\n"
|
text := "<i>… › Закупы › Создание</i>\n\n<b>Создание закупа</b>\n\n"
|
||||||
|
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
|
||||||
if len(page.Items) == 0 {
|
if len(page.Items) == 0 {
|
||||||
text += `Для создания закупа сначала нужно создать креатив.
|
text += `Для создания закупа сначала нужно создать креатив.
|
||||||
|
|
||||||
<i>Вернитесь назад и создайте креатив в разделе "Креативы"</i>`
|
Вернитесь назад и создайте креатив в разделе <b>Креативы</b>`
|
||||||
|
|
||||||
buttons = append(buttons, bot.Row(
|
buttons = append(buttons, bot.Row(
|
||||||
bot.Button("← Назад", "back"),
|
bot.Button("← Назад", "back"),
|
||||||
@@ -61,12 +62,12 @@ func (s *AddPurchase) renderCreativeSelection(b *bot.Bot, mode bot.RenderMode, j
|
|||||||
} else {
|
} else {
|
||||||
pageInfo := ""
|
pageInfo := ""
|
||||||
if page.Pages > 1 {
|
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)
|
`, pageInfo)
|
||||||
|
|
||||||
@@ -145,6 +146,7 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
b.SetState(&SelectChannelsForPurchase{
|
b.SetState(&SelectChannelsForPurchase{
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
ProjectID: s.ProjectID,
|
ProjectID: s.ProjectID,
|
||||||
|
ProjectTitle: s.ProjectTitle,
|
||||||
CreativeID: creativeID,
|
CreativeID: creativeID,
|
||||||
Channels: []PurchaseChannelInput{},
|
Channels: []PurchaseChannelInput{},
|
||||||
BackState: s.BackState,
|
BackState: s.BackState,
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
}
|
}
|
||||||
// Buttons, MediaType, MediaFileID остаются пустыми (не сохраняются в БД)
|
// Buttons, MediaType, MediaFileID остаются пустыми (не сохраняются в БД)
|
||||||
|
|
||||||
|
// Устанавливаем навигацию
|
||||||
|
s.Navigation = "… › Креативы › Детали"
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("creative_id", s.CreativeID).
|
Str("creative_id", s.CreativeID).
|
||||||
Str("name", creative.Name).
|
Str("name", creative.Name).
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package screens
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"example.com/m/bot"
|
"example.com/m/bot"
|
||||||
@@ -26,6 +27,9 @@ type CreativeEditorFields struct {
|
|||||||
MediaType string // "photo", "video", "animation"
|
MediaType string // "photo", "video", "animation"
|
||||||
MediaFileID string // File ID из Telegram
|
MediaFileID string // File ID из Telegram
|
||||||
|
|
||||||
|
// Навигация (хлебные крошки)
|
||||||
|
Navigation string
|
||||||
|
|
||||||
// Состояние для добавления кнопки
|
// Состояние для добавления кнопки
|
||||||
WaitingForButtonText bool
|
WaitingForButtonText bool
|
||||||
WaitingForButtonURL bool
|
WaitingForButtonURL bool
|
||||||
@@ -43,6 +47,13 @@ type CreativeEditorFields struct {
|
|||||||
|
|
||||||
// GetCreativeText возвращает текст для превью креатива
|
// GetCreativeText возвращает текст для превью креатива
|
||||||
func (e *CreativeEditorFields) GetCreativeText() string {
|
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 != "" {
|
if e.Text != nil && *e.Text != "" {
|
||||||
// Проверяем что текст не является "визуально пустым" HTML
|
// Проверяем что текст не является "визуально пустым" HTML
|
||||||
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
|
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
|
||||||
@@ -51,7 +62,7 @@ func (e *CreativeEditorFields) GetCreativeText() string {
|
|||||||
// Убираем пустые HTML-теги (простая проверка)
|
// Убираем пустые HTML-теги (простая проверка)
|
||||||
if text == "<a class=\"tg-link\"></a>" || text == "" {
|
if text == "<a class=\"tg-link\"></a>" || text == "" {
|
||||||
log.Info().Str("text", *e.Text).Msg("GetCreativeText: text is visually empty, using default")
|
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")
|
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")
|
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
|
return message.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tagSpan struct {
|
||||||
|
open string
|
||||||
|
close string
|
||||||
|
start int
|
||||||
|
end int
|
||||||
|
len int
|
||||||
|
}
|
||||||
|
|
||||||
// Преобразуем текст в руны для правильной работы с UTF-8
|
// Преобразуем текст в руны для правильной работы с UTF-8
|
||||||
runes := []rune(message.Text)
|
runes := []rune(message.Text)
|
||||||
result := ""
|
|
||||||
lastPos := 0
|
|
||||||
|
|
||||||
// Обрабатываем entities
|
var spans []tagSpan
|
||||||
for _, entity := range message.Entities {
|
for _, entity := range message.Entities {
|
||||||
// Добавляем текст до entity
|
// URL уже присутствует в сыром тексте, чтобы не дублировать - пропускаем entity.
|
||||||
if entity.Offset > lastPos {
|
if entity.Type == "url" {
|
||||||
result += string(runes[lastPos:entity.Offset])
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем текст entity
|
entityText := ""
|
||||||
entityText := string(runes[entity.Offset : entity.Offset+entity.Length])
|
if entity.Offset+entity.Length <= len(runes) {
|
||||||
|
entityText = string(runes[entity.Offset : entity.Offset+entity.Length])
|
||||||
|
}
|
||||||
|
|
||||||
// Оборачиваем в соответствующие HTML-теги
|
var openTag, closeTag string
|
||||||
switch entity.Type {
|
switch entity.Type {
|
||||||
case "bold":
|
case "bold":
|
||||||
result += "<b>" + entityText + "</b>"
|
openTag, closeTag = "<b>", "</b>"
|
||||||
case "italic":
|
case "italic":
|
||||||
result += "<i>" + entityText + "</i>"
|
openTag, closeTag = "<i>", "</i>"
|
||||||
case "underline":
|
case "underline":
|
||||||
result += "<u>" + entityText + "</u>"
|
openTag, closeTag = "<u>", "</u>"
|
||||||
case "strikethrough":
|
case "strikethrough":
|
||||||
result += "<s>" + entityText + "</s>"
|
openTag, closeTag = "<s>", "</s>"
|
||||||
case "code":
|
case "code":
|
||||||
result += "<code>" + entityText + "</code>"
|
openTag, closeTag = "<code>", "</code>"
|
||||||
case "pre":
|
case "pre":
|
||||||
result += "<pre>" + entityText + "</pre>"
|
openTag, closeTag = "<pre>", "</pre>"
|
||||||
case "text_link":
|
case "text_link":
|
||||||
if entity.URL != "" {
|
if entity.URL != "" {
|
||||||
result += fmt.Sprintf("<a href=\"%s\">%s</a>", entity.URL, entityText)
|
openTag = fmt.Sprintf("<a href=\"%s\">", entity.URL)
|
||||||
} else {
|
closeTag = "</a>"
|
||||||
result += entityText
|
|
||||||
}
|
}
|
||||||
case "url":
|
|
||||||
result += entityText
|
|
||||||
default:
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем оставшийся текст
|
opens := make(map[int][]tagSpan)
|
||||||
if lastPos < len(runes) {
|
closes := make(map[int][]tagSpan)
|
||||||
result += string(runes[lastPos:])
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const creativesPerPage = 6
|
|||||||
type Creatives struct {
|
type Creatives struct {
|
||||||
CurrentPage int
|
CurrentPage int
|
||||||
ProjectID string
|
ProjectID string
|
||||||
|
ProjectTitle string
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
@@ -49,29 +50,40 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
|
|||||||
var text string
|
var text string
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
|
||||||
if len(page.Items) == 0 {
|
// Навигация: 2 последних уровня
|
||||||
text = `<b>Креативы</b>
|
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>
|
<i>Креатив — это рекламный материал:</i>
|
||||||
▸ Текст объявления
|
‣ Текст объявления
|
||||||
▸ Медиа контент (фото, видео)
|
‣ Медиа контент (фото, видео)
|
||||||
▸ Шаблон для автопостинга
|
‣ Шаблон для автопостинга
|
||||||
▸ Варианты для A/B тестирования
|
‣ Варианты для A/B тестирования
|
||||||
|
|
||||||
<i>Начните с добавления первого креатива 👇</i>`
|
Начните с добавления первого креатива`, navPath)
|
||||||
} else {
|
} else {
|
||||||
pageInfo := ""
|
pageInfo := ""
|
||||||
if page.Pages > 1 {
|
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)
|
pageInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,27 @@ type MainMenu struct{}
|
|||||||
|
|
||||||
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
keyboard := bot.Keyboard(
|
keyboard := bot.Keyboard(
|
||||||
bot.Row(bot.Button("Мои проекты", "my_projects"), bot.Button("Поддержка", "support")),
|
bot.Row(
|
||||||
bot.Row(bot.Button("Закупы", "placements"), bot.Button("Дашборд", "dashboard")),
|
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) {
|
func (s *MainMenu) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||||
switch u.CallbackQuery.Data {
|
switch u.CallbackQuery.Data {
|
||||||
case "my_projects":
|
case "my_projects":
|
||||||
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.NewMessage)
|
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.NewMessage)
|
||||||
case "support":
|
case "purchases":
|
||||||
// b.SetState(&Support{}, bot.NewMessage)
|
b.SetState(&MyProjects{BackState: &MainMenu{}, OpenPurchases: true}, bot.NewMessage)
|
||||||
case "placements":
|
|
||||||
b.SetState(&MyPlacements{BackState: &MainMenu{}}, bot.NewMessage)
|
|
||||||
case "dashboard":
|
|
||||||
// b.SetState(&Dashboard{}, bot.NewMessage)
|
|
||||||
default:
|
default:
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,16 +66,20 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text := "<b>📍 Мои размещения</b>\n\n"
|
text := "<i>… › Главное меню › Размещения</i>\n\n<b>Мои размещения</b>\n\n"
|
||||||
|
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
|
||||||
if len(page.Items) == 0 {
|
if len(page.Items) == 0 {
|
||||||
text += `У вас пока нет размещений.
|
text += `У вас пока нет размещений
|
||||||
|
|
||||||
<i>Размещения создаются автоматически при создании закупа.</i>`
|
Размещения создаются автоматически при создании закупа`
|
||||||
} else {
|
} 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 в ряду
|
// Размещения по 2 в ряду
|
||||||
for i := 0; i < len(page.Items); i += 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(
|
buttons = append(buttons, bot.Row(
|
||||||
bot.Button("📊 Статистика", "statistics"),
|
bot.Button("▪ Статистика", "statistics"),
|
||||||
))
|
))
|
||||||
|
|
||||||
// Кнопка возврата в главное меню
|
// Кнопка возврата в главное меню
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type MyProjects struct {
|
|||||||
CurrentPage int
|
CurrentPage int
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
|
OpenPurchases bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
|
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 text string
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
navTitle := "Главное меню › Мои проекты"
|
||||||
if len(page.Items) == 0 {
|
headerTitle := "Мои проекты"
|
||||||
text = `<b>Мои проекты</b>
|
emptyStateText := `У вас пока нет подключенных каналов.
|
||||||
|
|
||||||
У вас пока нет подключенных каналов.
|
|
||||||
|
|
||||||
<i>Что дает подключение канала:</i>
|
<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 {
|
} else {
|
||||||
pageInfo := ""
|
pageInfo := ""
|
||||||
if page.Pages > 1 {
|
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>`,
|
%s`,
|
||||||
pageInfo)
|
navTitle,
|
||||||
|
headerTitle,
|
||||||
|
pageInfo,
|
||||||
|
nonEmptyText)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем слоты для проектов
|
// Создаем слоты для проектов
|
||||||
@@ -216,12 +231,21 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
return
|
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{
|
b.SetState(&ProjectDetails{
|
||||||
Project: project,
|
Project: project,
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
BackState: &MyProjects{WorkspaceID: s.WorkspaceID},
|
BackState: &MyProjects{WorkspaceID: s.WorkspaceID},
|
||||||
}, bot.NewMessage)
|
}, bot.NewMessage)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(data, "workspace:"):
|
case strings.HasPrefix(data, "workspace:"):
|
||||||
// Позволяет переключаться между workspace'ами в будущем
|
// Позволяет переключаться между workspace'ами в будущем
|
||||||
|
|||||||
@@ -34,33 +34,40 @@ func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
func (s *ProjectDetails) formatProjectDetails() string {
|
func (s *ProjectDetails) formatProjectDetails() string {
|
||||||
p := s.Project
|
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 != "" {
|
if p.Username != nil && *p.Username != "" {
|
||||||
text += fmt.Sprintf("Telegram-канал: @%s\n", *p.Username)
|
text += fmt.Sprintf("▸ Telegram: @%s\n", *p.Username)
|
||||||
} else {
|
} else {
|
||||||
text += "Telegram-канал: <i>не привязан</i>\n"
|
text += "▸ Telegram: <i>не привязан</i>\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Статус проекта
|
// Статус проекта
|
||||||
|
var statusSymbol string
|
||||||
var statusText string
|
var statusText string
|
||||||
switch p.Status {
|
switch p.Status {
|
||||||
case "active":
|
case "active":
|
||||||
statusText = "✓ Активный"
|
statusSymbol = "●"
|
||||||
|
statusText = "Активный"
|
||||||
case "inactive":
|
case "inactive":
|
||||||
statusText = "○ Неактивен"
|
statusSymbol = "○"
|
||||||
|
statusText = "Неактивен"
|
||||||
case "archived":
|
case "archived":
|
||||||
statusText = "▣ Архивный"
|
statusSymbol = "■"
|
||||||
|
statusText = "Архивный"
|
||||||
case "paused":
|
case "paused":
|
||||||
statusText = "⊙ Приостановлен"
|
statusSymbol = "◐"
|
||||||
|
statusText = "Приостановлен"
|
||||||
default:
|
default:
|
||||||
|
statusSymbol = "○"
|
||||||
statusText = p.Status
|
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
|
return text
|
||||||
}
|
}
|
||||||
@@ -126,6 +133,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
|
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
|
||||||
b.SetState(&Creatives{
|
b.SetState(&Creatives{
|
||||||
ProjectID: s.Project.ID,
|
ProjectID: s.Project.ID,
|
||||||
|
ProjectTitle: s.Project.Title,
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}, bot.NewMessage)
|
}, bot.NewMessage)
|
||||||
@@ -135,6 +143,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
b.SetState(&Purchases{
|
b.SetState(&Purchases{
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
ProjectID: s.Project.ID,
|
ProjectID: s.Project.ID,
|
||||||
|
ProjectTitle: s.Project.Title,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}, bot.NewMessage)
|
}, bot.NewMessage)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type PurchaseDetails struct {
|
type PurchaseDetails struct {
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
ProjectID string
|
ProjectID string
|
||||||
|
ProjectTitle string
|
||||||
PurchaseID string
|
PurchaseID string
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
@@ -53,21 +54,23 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
|
|||||||
creative = nil
|
creative = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
text := "<b>📦 Детали закупа</b>\n\n"
|
text := "<i>… › Закупы › Детали</i>\n\n<b>Детали закупа</b>\n\n"
|
||||||
|
|
||||||
// Информация о креативе
|
// Информация о креативе
|
||||||
if creative != nil {
|
if creative != nil {
|
||||||
text += fmt.Sprintf("<b>Креатив:</b> %s\n", creative.Name)
|
text += fmt.Sprintf("▪ <b>Креатив:</b> %s\n", creative.Name)
|
||||||
} else {
|
} else {
|
||||||
text += fmt.Sprintf("<b>Креатив ID:</b> %s\n", purchase.CreativeID)
|
text += fmt.Sprintf("▪ <b>Креатив ID:</b> %s\n", purchase.CreativeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Статус закупа
|
// Статус закупа
|
||||||
|
statusSymbol := "●"
|
||||||
statusText := "Активный"
|
statusText := "Активный"
|
||||||
if purchase.Status == "archived" {
|
if purchase.Status == "archived" {
|
||||||
|
statusSymbol = "■"
|
||||||
statusText = "Архивирован"
|
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 {
|
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))
|
text += fmt.Sprintf("<b>Каналы (%d):</b>\n\n", len(purchase.Channels))
|
||||||
|
|
||||||
for i, ch := range purchase.Channels {
|
for i, ch := range purchase.Channels {
|
||||||
// Определяем статус emoji
|
// Определяем статус
|
||||||
statusEmoji := "📌"
|
statusSymbol := "○"
|
||||||
switch ch.Status {
|
switch ch.Status {
|
||||||
case "planned":
|
case "planned":
|
||||||
statusEmoji = "📝"
|
statusSymbol = "○"
|
||||||
case "approved":
|
case "approved":
|
||||||
statusEmoji = "✅"
|
statusSymbol = "◉"
|
||||||
case "rejected":
|
case "rejected":
|
||||||
statusEmoji = "❌"
|
statusSymbol = "⊗"
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
statusEmoji = "🔄"
|
statusSymbol = "◐"
|
||||||
case "completed":
|
case "completed":
|
||||||
statusEmoji = "✔️"
|
statusSymbol = "●"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Название канала
|
// Название канала
|
||||||
@@ -99,16 +102,16 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
|
|||||||
channelName = "@" + *ch.Channel.Username
|
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 {
|
if ch.PlannedCost != nil {
|
||||||
text += fmt.Sprintf(" 💰 Стоимость: %.0f₽\n", *ch.PlannedCost)
|
text += fmt.Sprintf(" ▸ Стоимость: %.0f₽\n", *ch.PlannedCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Комментарий
|
// Комментарий
|
||||||
if ch.Comment != nil && *ch.Comment != "" {
|
if ch.Comment != nil && *ch.Comment != "" {
|
||||||
text += fmt.Sprintf(" 💬 %s\n", *ch.Comment)
|
text += fmt.Sprintf(" ▸ %s\n", *ch.Comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тип invite link
|
// Тип invite link
|
||||||
@@ -116,7 +119,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
|
|||||||
if ch.InviteLinkType == "approval" {
|
if ch.InviteLinkType == "approval" {
|
||||||
inviteLinkTypeText = "с одобрением"
|
inviteLinkTypeText = "с одобрением"
|
||||||
}
|
}
|
||||||
text += fmt.Sprintf(" 🔗 Ссылка: %s\n", inviteLinkTypeText)
|
text += fmt.Sprintf(" ▸ Ссылка: %s\n", inviteLinkTypeText)
|
||||||
|
|
||||||
text += "\n"
|
text += "\n"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type Purchases struct {
|
type Purchases struct {
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
ProjectID string
|
ProjectID string
|
||||||
|
ProjectTitle string
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +40,12 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
|
|||||||
return
|
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
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
|
||||||
@@ -47,24 +53,24 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
|
|||||||
text += `Список закупов пуст.
|
text += `Список закупов пуст.
|
||||||
|
|
||||||
<i>Закуп — это план размещения рекламы в каналах:</i>
|
<i>Закуп — это план размещения рекламы в каналах:</i>
|
||||||
▸ Выбор креатива
|
‣ Выбор креатива
|
||||||
▸ Список каналов для размещения
|
‣ Список каналов для размещения
|
||||||
▸ Планируемая стоимость
|
‣ Планируемая стоимость
|
||||||
▸ Статус выполнения
|
‣ Статус выполнения
|
||||||
|
|
||||||
<i>Создайте первый закуп 👇</i>`
|
Создайте первый закуп`
|
||||||
} else {
|
} else {
|
||||||
text += "Управление закупами для проекта.\n\n"
|
text += "Управление закупами для проекта.\n\n"
|
||||||
text += "<i>Выберите закуп для деталей:</i>\n\n"
|
text += "Выберите закуп для деталей:\n\n"
|
||||||
|
|
||||||
for i, purchase := range purchases {
|
for i, purchase := range purchases {
|
||||||
// Определяем статус emoji
|
// Определяем статус
|
||||||
statusEmoji := "📌"
|
statusSymbol := "●"
|
||||||
switch purchase.Status {
|
switch purchase.Status {
|
||||||
case "active":
|
case "active":
|
||||||
statusEmoji = "🟢"
|
statusSymbol = "●"
|
||||||
case "archived":
|
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 каналов)",
|
buttonText := fmt.Sprintf("%s Закуп #%d (%d/%d каналов)",
|
||||||
statusEmoji,
|
statusSymbol,
|
||||||
i+1,
|
i+1,
|
||||||
completedChannels,
|
completedChannels,
|
||||||
totalChannels,
|
totalChannels,
|
||||||
@@ -134,6 +140,7 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
b.SetState(&AddPurchase{
|
b.SetState(&AddPurchase{
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
ProjectID: s.ProjectID,
|
ProjectID: s.ProjectID,
|
||||||
|
ProjectTitle: s.ProjectTitle,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}, bot.EditMessage)
|
}, bot.EditMessage)
|
||||||
|
|
||||||
@@ -144,6 +151,7 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
b.SetState(&PurchaseDetails{
|
b.SetState(&PurchaseDetails{
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
ProjectID: s.ProjectID,
|
ProjectID: s.ProjectID,
|
||||||
|
ProjectTitle: s.ProjectTitle,
|
||||||
PurchaseID: purchaseID,
|
PurchaseID: purchaseID,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}, bot.NewMessage)
|
}, bot.NewMessage)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type PurchaseChannelInput struct {
|
|||||||
type SelectChannelsForPurchase struct {
|
type SelectChannelsForPurchase struct {
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
ProjectID string
|
ProjectID string
|
||||||
|
ProjectTitle string
|
||||||
CreativeID string
|
CreativeID string
|
||||||
Channels []PurchaseChannelInput
|
Channels []PurchaseChannelInput
|
||||||
BackState bot.State
|
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) {
|
func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.RenderMode) {
|
||||||
text := "<b>➕ Создание закупа</b>\n\n"
|
text := "<i>… › Создание › Каналы</i>\n\n"
|
||||||
text += "<b>Шаг 2/2: Добавление каналов</b>\n\n"
|
text += "<b>Создание закупа</b>\n\n"
|
||||||
|
text += "<b>Шаг 2/2:</b> Добавление каналов\n\n"
|
||||||
|
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
|
||||||
if len(s.Channels) == 0 {
|
if len(s.Channels) == 0 {
|
||||||
text += `Добавьте каналы для размещения рекламы.
|
text += `Добавьте каналы для размещения рекламы
|
||||||
|
|
||||||
<i>Отправьте username канала (без @)</i>
|
Отправьте username канала (без @)
|
||||||
Например: <code>channel_name</code>
|
Например: <code>channel_name</code>
|
||||||
|
|
||||||
После добавления всех каналов нажмите "Создать закуп"`
|
После добавления всех каналов нажмите <b>Создать закуп</b>`
|
||||||
} else {
|
} 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 {
|
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 {
|
if ch.PlannedCost != nil {
|
||||||
channelText += fmt.Sprintf(" (%.0f₽)", *ch.PlannedCost)
|
channelText += fmt.Sprintf(" — %.0f₽", *ch.PlannedCost)
|
||||||
}
|
}
|
||||||
text += channelText + "\n"
|
text += channelText + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
text += "\n<i>Добавьте еще каналы или создайте закуп</i>"
|
text += "\nДобавьте еще каналы или создайте закуп"
|
||||||
|
|
||||||
// Кнопка создания закупа (доступна только если есть каналы)
|
// Кнопка создания закупа (доступна только если есть каналы)
|
||||||
buttons = append(buttons, bot.Row(
|
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{
|
b.SetState(&AddPurchase{
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
ProjectID: s.ProjectID,
|
ProjectID: s.ProjectID,
|
||||||
|
ProjectTitle: s.ProjectTitle,
|
||||||
BackState: s.BackState,
|
BackState: s.BackState,
|
||||||
}, bot.EditMessage)
|
}, 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>
|
||||||
<b>Креатив:</b> %s
|
|
||||||
<b>Каналов:</b> %d
|
|
||||||
|
|
||||||
Закуп создан и готов к работе.`, 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(
|
b.Edit(successText, bot.Keyboard(
|
||||||
bot.Row(bot.Button("✅ Готово", "done")),
|
bot.Row(bot.Button("● Готово", "done")),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user