diff --git a/telegram_bot/bot/bot.go b/telegram_bot/bot/bot.go
index aa87022..cda95ac 100644
--- a/telegram_bot/bot/bot.go
+++ b/telegram_bot/bot/bot.go
@@ -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,
}
diff --git a/telegram_bot/screens/add_creative.go b/telegram_bot/screens/add_creative.go
index be5a59e..84b8a7b 100644
--- a/telegram_bot/screens/add_creative.go
+++ b/telegram_bot/screens/add_creative.go
@@ -20,14 +20,16 @@ type AddCreative struct {
}
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
- text := `+ Добавить креатив
+ text := `… › Креативы › Добавление
+
++ Добавить креатив
Введите название креатива:
Например:
-• Летняя акция
-• Баннер 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)
diff --git a/telegram_bot/screens/add_purchase.go b/telegram_bot/screens/add_purchase.go
index ba50a4f..048af95 100644
--- a/telegram_bot/screens/add_purchase.go
+++ b/telegram_bot/screens/add_purchase.go
@@ -12,10 +12,11 @@ import (
const addPurchaseCreativesPerPage = 5
type AddPurchase struct {
- WorkspaceID string
- ProjectID string
- CurrentPage int
- BackState bot.State
+ WorkspaceID string
+ ProjectID string
+ ProjectTitle string
+ CurrentPage int
+ BackState bot.State
}
func (s *AddPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
@@ -46,14 +47,14 @@ func (s *AddPurchase) renderCreativeSelection(b *bot.Bot, mode bot.RenderMode, j
return
}
- text := "➕ Создание закупа\n\n"
+ text := "… › Закупы › Создание\n\nСоздание закупа\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text += `Для создания закупа сначала нужно создать креатив.
-Вернитесь назад и создайте креатив в разделе "Креативы"`
+Вернитесь назад и создайте креатив в разделе Креативы`
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(" (стр. %d/%d)", page.Page, page.Pages)
}
- text += fmt.Sprintf(`Шаг 1/2: Выбор креатива%s
+ text += fmt.Sprintf(`Шаг 1/2: Выбор креатива%s
-Выберите креатив для закупа:
+Выберите креатив для закупа
`, pageInfo)
@@ -143,11 +144,12 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
// Переходим к выбору каналов
b.SetState(&SelectChannelsForPurchase{
- WorkspaceID: s.WorkspaceID,
- ProjectID: s.ProjectID,
- CreativeID: creativeID,
- Channels: []PurchaseChannelInput{},
- BackState: s.BackState,
+ WorkspaceID: s.WorkspaceID,
+ ProjectID: s.ProjectID,
+ ProjectTitle: s.ProjectTitle,
+ CreativeID: creativeID,
+ Channels: []PurchaseChannelInput{},
+ BackState: s.BackState,
}, bot.EditMessage)
} else {
s.Enter(b, bot.NewMessage)
diff --git a/telegram_bot/screens/creative_details.go b/telegram_bot/screens/creative_details.go
index 4ec31e7..6771fa9 100644
--- a/telegram_bot/screens/creative_details.go
+++ b/telegram_bot/screens/creative_details.go
@@ -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).
diff --git a/telegram_bot/screens/creative_editor.go b/telegram_bot/screens/creative_editor.go
index 56d1941..09928f6 100644
--- a/telegram_bot/screens/creative_editor.go
+++ b/telegram_bot/screens/creative_editor.go
@@ -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("%s\n\n", e.Navigation)
+ }
+
if e.Text != nil && *e.Text != "" {
// Проверяем что текст не является "визуально пустым" HTML
// Например: "" - пустая ссылка
@@ -51,7 +62,7 @@ func (e *CreativeEditorFields) GetCreativeText() string {
// Убираем пустые HTML-теги (простая проверка)
if text == "" || text == "" {
log.Info().Str("text", *e.Text).Msg("GetCreativeText: text is visually empty, using default")
- return `💭 Я твой креатив
+ return result + `💭 Я твой креатив
У меня пока нет текста, добавь его ниже.
@@ -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 `💭 Я твой креатив
+ return result + `💭 Я твой креатив
У меня пока нет текста, добавь его ниже.
@@ -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 += "" + entityText + ""
+ openTag, closeTag = "", ""
case "italic":
- result += "" + entityText + ""
+ openTag, closeTag = "", ""
case "underline":
- result += "" + entityText + ""
+ openTag, closeTag = "", ""
case "strikethrough":
- result += "" + entityText + ""
+ openTag, closeTag = "", ""
case "code":
- result += "" + entityText + ""
+ openTag, closeTag = "", ""
case "pre":
- result += "" + entityText + "
"
+ openTag, closeTag = "", "
"
case "text_link":
if entity.URL != "" {
- result += fmt.Sprintf("%s", entity.URL, entityText)
- } else {
- result += entityText
+ openTag = fmt.Sprintf("", entity.URL)
+ closeTag = ""
}
- 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()
}
diff --git a/telegram_bot/screens/creatives.go b/telegram_bot/screens/creatives.go
index 98d894e..e358932 100644
--- a/telegram_bot/screens/creatives.go
+++ b/telegram_bot/screens/creatives.go
@@ -13,10 +13,11 @@ import (
const creativesPerPage = 6
type Creatives struct {
- CurrentPage int
- ProjectID string
- WorkspaceID string
- BackState bot.State
+ CurrentPage int
+ ProjectID string
+ ProjectTitle string
+ WorkspaceID string
+ BackState bot.State
}
func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) {
@@ -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 = `Креативы
+ // Навигация: 2 последних уровня
+ navPath := "… › Креативы"
+ if s.ProjectTitle != "" {
+ navPath = fmt.Sprintf("… › %s › Креативы", s.ProjectTitle)
+ }
-У вас пока нет креативов.
+ if len(page.Items) == 0 {
+ text = fmt.Sprintf(`%s
+
+Креативы
+
+У вас пока нет креативов
Креатив — это рекламный материал:
-▸ Текст объявления
-▸ Медиа контент (фото, видео)
-▸ Шаблон для автопостинга
-▸ Варианты для A/B тестирования
+ ‣ Текст объявления
+ ‣ Медиа контент (фото, видео)
+ ‣ Шаблон для автопостинга
+ ‣ Варианты для A/B тестирования
-Начните с добавления первого креатива 👇`
+Начните с добавления первого креатива`, navPath)
} else {
pageInfo := ""
if page.Pages > 1 {
- pageInfo = fmt.Sprintf("\nСтраница %d из %d", page.Page, page.Pages)
+ pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages)
}
- text = fmt.Sprintf(`Креативы
+ text = fmt.Sprintf(`%s
-Управление рекламными материалами.%s
+Креативы%s
-Выберите креатив 👇`,
+Управление рекламными материалами
+
+Выберите креатив`,
+ navPath,
pageInfo)
}
diff --git a/telegram_bot/screens/main_menu.go b/telegram_bot/screens/main_menu.go
index a3730fe..f74b5e1 100644
--- a/telegram_bot/screens/main_menu.go
+++ b/telegram_bot/screens/main_menu.go
@@ -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 := "Главное меню\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)
}
diff --git a/telegram_bot/screens/my_placements.go b/telegram_bot/screens/my_placements.go
index d360731..e77d98a 100644
--- a/telegram_bot/screens/my_placements.go
+++ b/telegram_bot/screens/my_placements.go
@@ -66,16 +66,20 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
return
}
- text := "📍 Мои размещения\n\n"
+ text := "… › Главное меню › Размещения\n\nМои размещения\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
- text += `У вас пока нет размещений.
+ text += `У вас пока нет размещений
-Размещения создаются автоматически при создании закупа.`
+Размещения создаются автоматически при создании закупа`
} else {
- text += fmt.Sprintf("Выберите размещение для деталей (стр. %d из %d):\n\n", s.CurrentPage+1, page.Pages)
+ pageInfo := ""
+ if page.Pages > 1 {
+ pageInfo = fmt.Sprintf(" (стр. %d/%d)", 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"),
))
// Кнопка возврата в главное меню
diff --git a/telegram_bot/screens/my_projects.go b/telegram_bot/screens/my_projects.go
index 22197a2..f88e108 100644
--- a/telegram_bot/screens/my_projects.go
+++ b/telegram_bot/screens/my_projects.go
@@ -13,9 +13,10 @@ import (
const projectsPerPage = 6
type MyProjects struct {
- CurrentPage int
- WorkspaceID string
- BackState bot.State
+ 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 = `Мои проекты
-
-У вас пока нет подключенных каналов.
+ navTitle := "Главное меню › Мои проекты"
+ headerTitle := "Мои проекты"
+ emptyStateText := `У вас пока нет подключенных каналов.
Что дает подключение канала:
-▸ Управление типом приглашений (публичные/с одобрением)
-▸ Автоматическая отправка креативов в канал
-▸ Интеграция с планом закупов
-▸ Статистика и аналитика подписчиков
-▸ Настройка уведомлений
+ ▸ Управление типом приглашений (публичные/с одобрением)
+ ▸ Автоматическая отправка креативов в канал
+ ▸ Интеграция с планом закупов
+ ▸ Статистика и аналитика подписчиков
+ ▸ Настройка уведомлений
-Начните с добавления первого проекта ↓`
+Начните с добавления первого проекта`
+ nonEmptyText := "Управление вашими Telegram-каналами.\n\nВыберите канал"
+ if s.OpenPurchases {
+ navTitle = "Главное меню › Закупы"
+ headerTitle = "Закупы"
+ nonEmptyText = "Выберите проект для просмотра закупов"
+ }
+
+ if len(page.Items) == 0 {
+ text = fmt.Sprintf(`%s
+
+%s
+
+%s`, navTitle, headerTitle, emptyStateText)
} else {
pageInfo := ""
if page.Pages > 1 {
- pageInfo = fmt.Sprintf("\nСтраница %d из %d", page.Page, page.Pages)
+ pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages)
}
- text = fmt.Sprintf(`Мои проекты
+ text = fmt.Sprintf(`%s
-Управление вашими Telegram-каналами.%s
+%s%s
-Выберите канал 👇 `,
- pageInfo)
+%s`,
+ navTitle,
+ headerTitle,
+ pageInfo,
+ nonEmptyText)
}
// Создаем слоты для проектов
@@ -216,11 +231,20 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
return
}
- b.SetState(&ProjectDetails{
- Project: project,
- WorkspaceID: s.WorkspaceID,
- BackState: &MyProjects{WorkspaceID: s.WorkspaceID},
- }, bot.NewMessage)
+ 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:"):
diff --git a/telegram_bot/screens/project_details.go b/telegram_bot/screens/project_details.go
index 15551d4..e6930ca 100644
--- a/telegram_bot/screens/project_details.go
+++ b/telegram_bot/screens/project_details.go
@@ -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("%s\n\n", p.Title)
+ // Хлебные крошки и заголовок
+ text := fmt.Sprintf("… › Мои проекты › %s\n\n", p.Title)
+ text += fmt.Sprintf("%s\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-канал: не привязан\n"
+ text += "▸ Telegram: не привязан\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Управление проектом 👇"
+ text += "\nУправление проектом"
return text
}
@@ -125,17 +132,19 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
case strings.HasPrefix(data, "creatives:"):
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
b.SetState(&Creatives{
- ProjectID: s.Project.ID,
- WorkspaceID: s.WorkspaceID,
- BackState: s,
+ ProjectID: s.Project.ID,
+ ProjectTitle: s.Project.Title,
+ WorkspaceID: s.WorkspaceID,
+ BackState: s,
}, bot.NewMessage)
case strings.HasPrefix(data, "purchases:"):
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
b.SetState(&Purchases{
- WorkspaceID: s.WorkspaceID,
- ProjectID: s.Project.ID,
- BackState: s,
+ WorkspaceID: s.WorkspaceID,
+ ProjectID: s.Project.ID,
+ ProjectTitle: s.Project.Title,
+ BackState: s,
}, bot.NewMessage)
case data == "back_to_project":
diff --git a/telegram_bot/screens/purchase_details.go b/telegram_bot/screens/purchase_details.go
index 1491577..3186f5a 100644
--- a/telegram_bot/screens/purchase_details.go
+++ b/telegram_bot/screens/purchase_details.go
@@ -10,10 +10,11 @@ import (
)
type PurchaseDetails struct {
- WorkspaceID string
- ProjectID string
- PurchaseID string
- BackState bot.State
+ WorkspaceID string
+ ProjectID string
+ ProjectTitle string
+ PurchaseID string
+ BackState bot.State
}
func (s *PurchaseDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
@@ -53,21 +54,23 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
creative = nil
}
- text := "📦 Детали закупа\n\n"
+ text := "… › Закупы › Детали\n\nДетали закупа\n\n"
// Информация о креативе
if creative != nil {
- text += fmt.Sprintf("Креатив: %s\n", creative.Name)
+ text += fmt.Sprintf("▪ Креатив: %s\n", creative.Name)
} else {
- text += fmt.Sprintf("Креатив ID: %s\n", purchase.CreativeID)
+ text += fmt.Sprintf("▪ Креатив ID: %s\n", purchase.CreativeID)
}
// Статус закупа
+ statusSymbol := "●"
statusText := "Активный"
if purchase.Status == "archived" {
+ statusSymbol = "■"
statusText = "Архивирован"
}
- text += fmt.Sprintf("Статус: %s\n\n", statusText)
+ text += fmt.Sprintf("▪ Статус: %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("Каналы (%d):\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 %s\n", i+1, statusEmoji, channelName)
+ text += fmt.Sprintf("%d. %s %s\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"
}
diff --git a/telegram_bot/screens/purchases.go b/telegram_bot/screens/purchases.go
index 76596c6..e9fd615 100644
--- a/telegram_bot/screens/purchases.go
+++ b/telegram_bot/screens/purchases.go
@@ -10,9 +10,10 @@ import (
)
type Purchases struct {
- WorkspaceID string
- ProjectID string
- BackState bot.State
+ WorkspaceID string
+ ProjectID string
+ ProjectTitle string
+ BackState bot.State
}
func (s *Purchases) Enter(b *bot.Bot, mode bot.RenderMode) {
@@ -39,7 +40,12 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
return
}
- text := "📦 Закупы\n\n"
+ // Навигация: 2 последних уровня
+ navPath := "… › Закупы"
+ if s.ProjectTitle != "" {
+ navPath = fmt.Sprintf("… › %s › Закупы", s.ProjectTitle)
+ }
+ text := fmt.Sprintf("%s\n\nЗакупы\n\n", navPath)
var buttons [][]echotron.InlineKeyboardButton
@@ -47,24 +53,24 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string)
text += `Список закупов пуст.
Закуп — это план размещения рекламы в каналах:
-▸ Выбор креатива
-▸ Список каналов для размещения
-▸ Планируемая стоимость
-▸ Статус выполнения
+ ‣ Выбор креатива
+ ‣ Список каналов для размещения
+ ‣ Планируемая стоимость
+ ‣ Статус выполнения
-Создайте первый закуп 👇`
+Создайте первый закуп`
} else {
text += "Управление закупами для проекта.\n\n"
- text += "Выберите закуп для деталей:\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,
@@ -132,9 +138,10 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
case "add_purchase":
b.SetState(&AddPurchase{
- WorkspaceID: s.WorkspaceID,
- ProjectID: s.ProjectID,
- BackState: s,
+ WorkspaceID: s.WorkspaceID,
+ ProjectID: s.ProjectID,
+ ProjectTitle: s.ProjectTitle,
+ BackState: s,
}, bot.EditMessage)
default:
@@ -142,10 +149,11 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
if len(data) > 9 && data[:9] == "purchase:" {
purchaseID := data[9:]
b.SetState(&PurchaseDetails{
- WorkspaceID: s.WorkspaceID,
- ProjectID: s.ProjectID,
- PurchaseID: purchaseID,
- BackState: s,
+ WorkspaceID: s.WorkspaceID,
+ ProjectID: s.ProjectID,
+ ProjectTitle: s.ProjectTitle,
+ PurchaseID: purchaseID,
+ BackState: s,
}, bot.NewMessage)
} else {
s.Enter(b, bot.NewMessage)
diff --git a/telegram_bot/screens/select_channels_for_purchase.go b/telegram_bot/screens/select_channels_for_purchase.go
index fd8ebae..c0a5c13 100644
--- a/telegram_bot/screens/select_channels_for_purchase.go
+++ b/telegram_bot/screens/select_channels_for_purchase.go
@@ -18,11 +18,12 @@ type PurchaseChannelInput struct {
}
type SelectChannelsForPurchase struct {
- WorkspaceID string
- ProjectID string
- CreativeID string
- Channels []PurchaseChannelInput
- BackState bot.State
+ WorkspaceID string
+ ProjectID string
+ ProjectTitle string
+ CreativeID string
+ Channels []PurchaseChannelInput
+ BackState bot.State
}
func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
@@ -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 := "➕ Создание закупа\n\n"
- text += "Шаг 2/2: Добавление каналов\n\n"
+ text := "… › Создание › Каналы\n\n"
+ text += "Создание закупа\n\n"
+ text += "Шаг 2/2: Добавление каналов\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(s.Channels) == 0 {
- text += `Добавьте каналы для размещения рекламы.
+ text += `Добавьте каналы для размещения рекламы
-Отправьте username канала (без @)
+Отправьте username канала (без @)
Например: channel_name
-После добавления всех каналов нажмите "Создать закуп"`
+После добавления всех каналов нажмите Создать закуп`
} else {
- text += fmt.Sprintf("Добавлено каналов: %d\n\n", len(s.Channels))
+ text += fmt.Sprintf("Добавлено каналов: %d\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Добавьте еще каналы или создайте закуп"
+ text += "\nДобавьте еще каналы или создайте закуп"
// Кнопка создания закупа (доступна только если есть каналы)
buttons = append(buttons, bot.Row(
- bot.Button("✅ Создать закуп", "create"),
+ bot.Button("● Создать закуп", "create"),
))
}
@@ -95,9 +97,10 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
case "back":
// Возвращаемся к выбору креатива
b.SetState(&AddPurchase{
- WorkspaceID: s.WorkspaceID,
- ProjectID: s.ProjectID,
- BackState: s.BackState,
+ WorkspaceID: s.WorkspaceID,
+ ProjectID: s.ProjectID,
+ ProjectTitle: s.ProjectTitle,
+ BackState: s.BackState,
}, bot.EditMessage)
case "create":
@@ -159,16 +162,18 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
}
// Показываем успешное создание
- successText := fmt.Sprintf(`✅ Закуп создан!
+ successText := fmt.Sprintf(`… › Создание › Готово
-ID: %s
-Креатив: %s
-Каналов: %d
+Закуп создан
-Закуп создан и готов к работе.`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
+ ▪ ID: %s
+ ▪ Креатив: %s
+ ▪ Каналов: %d
+
+Закуп создан и готов к работе`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
b.Edit(successText, bot.Keyboard(
- bot.Row(bot.Button("✅ Готово", "done")),
+ bot.Row(bot.Button("● Готово", "done")),
))
}