From 922067d1947b579a229203665fabafeee5db7e88 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Sat, 3 Jan 2026 11:24:24 +0300 Subject: [PATCH] bot changes --- telegram_bot/bot/bot.go | 5 + telegram_bot/screens/add_creative.go | 13 ++- telegram_bot/screens/add_purchase.go | 30 ++--- telegram_bot/screens/creative_details.go | 3 + telegram_bot/screens/creative_editor.go | 104 +++++++++++++----- telegram_bot/screens/creatives.go | 44 +++++--- telegram_bot/screens/main_menu.go | 22 ++-- telegram_bot/screens/my_placements.go | 14 ++- telegram_bot/screens/my_projects.go | 72 ++++++++---- telegram_bot/screens/project_details.go | 41 ++++--- telegram_bot/screens/purchase_details.go | 41 +++---- telegram_bot/screens/purchases.go | 52 +++++---- .../screens/select_channels_for_purchase.go | 53 +++++---- 13 files changed, 312 insertions(+), 182 deletions(-) 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")), )) }