package screens import ( "context" "fmt" "sort" "strings" "github.com/NicoNex/echotron/v3" "github.com/TelegramExchange/tgex-backend/tg_bot/backend" "github.com/TelegramExchange/tgex-backend/tg_bot/bot" "github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui" "github.com/rs/zerolog/log" ) type CreativeDetails struct { CreativeEditorFields // Встраивание общих полей CreativeID string ProjectID string ProjectTitle string // Название проекта для сообщений BackState bot.State // View mode: открыты из режима просмотра ViewMode bool // Открыты из CreativeView ViewBackTo bot.State // Состояние для возврата (CreativeView) // Флаг подтверждения удаления confirmingDelete bool } func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) { creative, err := b.Backend.GetCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CreativeID) if err != nil { log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative") b.SendNew("❌ Не удалось загрузить креатив", Keyboard( Row(Button("← Назад", "back")), )) return } // Заполняем поля из загруженного креатива s.Name = &creative.Name if creative.Text != "" { sanitizedText := ui.SanitizeHTML(creative.Text) s.Text = &sanitizedText log.Info().Str("creative_text", creative.Text).Msg("Set creative text from backend") } else { log.Info().Msg("Creative text is empty from backend") } s.MediaItems = nil if len(creative.MediaItems) > 0 { mediaItems := append([]backend.CreativeMediaItem(nil), creative.MediaItems...) sort.Slice(mediaItems, func(i, j int) bool { return mediaItems[i].Position < mediaItems[j].Position }) for _, item := range mediaItems { s.MediaItems = append(s.MediaItems, MediaItem{ MediaType: item.MediaType, MediaFileID: item.MediaFileID, }) } } s.Buttons = nil for _, btn := range creative.Buttons { s.Buttons = append(s.Buttons, InlineButton{Text: btn.Text, URL: btn.URL}) } if creative.Tag != "" { s.Tag = &creative.Tag } s.MediaChanged = false s.ClearInputMode() log.Info(). Str("creative_id", s.CreativeID). Str("name", creative.Name). Bool("has_text", creative.Text != ""). Bool("s_text_is_nil", s.Text == nil). Msg("Sending creative preview and panel") // Отправляем превью и панель управления s.SendCreativePreview(b) s.showManagementPanelWithDelete(b) } func (s *CreativeDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { if u.CallbackQuery == nil || u.CallbackQuery.Data == "" { return } data := u.CallbackQuery.Data // Обрабатываем общие callbacks (edit_text, add_button, add_media, delete_button, delete_media) switch { case data == "cancel", data == "back": // Удаляем сообщения текущего экрана перед переходом s.CleanupMessages(b) if s.ViewMode && s.ViewBackTo != nil { b.SetState(s.ViewBackTo, bot.NewMessage) } else if s.BackState != nil { b.SetState(s.BackState, bot.NewMessage) } case data == "edit_text": s.InputMode = inputModeText s.ShowTextEditPanel(b) case data == "add_button": s.InputMode = inputModeButtonText s.PendingButtonType = "invite" s.ShowAddButtonPanel(b) case data == "add_media": s.InputMode = inputModeMedia s.ShowMediaPanel(b) case data == "delete_media": s.MediaItems = nil s.MediaChanged = true s.UpdateCreativePreview(b) s.showManagementPanelWithDelete(b) case data == "cancel_add_button": // Отменяем добавление кнопки s.CancelPendingButton(b) s.showManagementPanelWithDelete(b) case data == "button_type_invite": s.PendingButtonType = "invite" s.InputMode = inputModeButtonText s.ShowAddButtonPanel(b) case data == "button_type_custom": s.PendingButtonType = "custom" s.InputMode = inputModeButtonText s.ShowAddButtonPanel(b) case data == "cancel_edit", data == "cancel_media": s.ClearInputMode() s.showManagementPanelWithDelete(b) case data == "confirm_save": s.updateCreative(b) case data == "delete_creative": if !s.confirmingDelete { // Первое нажатие - просим подтверждение s.confirmingDelete = true s.showManagementPanelWithDelete(b) } else { // Второе нажатие - удаляем s.deleteCreative(b) } case data == "confirm_delete": // Это только для безопасности, главная логика в delete_creative s.deleteCreative(b) case strings.HasPrefix(data, "delete_button:"): parts := strings.Split(data, ":") if len(parts) == 2 { var index int if _, err := fmt.Sscanf(parts[1], "%d", &index); err == nil { if index >= 0 && index < len(s.Buttons) { s.Buttons = append(s.Buttons[:index], s.Buttons[index+1:]...) s.UpdateCreativePreview(b) s.showManagementPanelWithDelete(b) } } } default: // Сбрасываем флаг подтверждения удаления при любом другом действии if s.confirmingDelete { s.confirmingDelete = false s.showManagementPanelWithDelete(b) } } } func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) { if u.Message == nil { return } switch s.InputMode { case inputModeMedia: if !s.SetMediaFromMessage(u.Message) { return } if u.Message.MediaGroupID != "" { groupID := u.Message.MediaGroupID s.scheduleMediaGroupAction(groupID, func() { s.UpdateCreativePreview(b) s.ShowMediaPanel(b) }) b.MarkHandled() return } s.UpdateCreativePreview(b) s.ShowMediaPanel(b) return case inputModeButtonText: if u.Message.Text == "" { return } buttonText := u.Message.Text s.DeleteUserMessage(b, u.Message.ID) // Создаём кнопку с placeholder URL if s.PendingButtonType == "custom" { s.AddCustomButtonPlaceholder(buttonText) s.UpdateCreativePreview(b) s.InputMode = inputModeButtonURL s.ShowButtonURLPanel(b) } else { s.AddInviteButton(buttonText) s.UpdateCreativePreview(b) s.PendingButtonType = "" s.ClearInputMode() s.showManagementPanelWithDelete(b) } return case inputModeButtonURL: if u.Message.Text == "" { return } url := strings.TrimSpace(u.Message.Text) s.DeleteUserMessage(b, u.Message.ID) // Простая валидация URL if !s.IsValidButtonURL(url, true) { s.ShowInvalidButtonURLPanel(b) return } // Обновляем URL последней кнопки s.UpdateLastButtonURL(url) s.ClearInputMode() s.PendingButtonType = "" s.UpdateCreativePreview(b) s.showManagementPanelWithDelete(b) return case inputModeText: if u.Message.Text == "" { return } text := ui.FormatMessageHTML(u.Message) s.Text = &text s.DeleteUserMessage(b, u.Message.ID) s.UpdateCreativePreview(b) s.ClearInputMode() s.showManagementPanelWithDelete(b) return } // Обрабатываем имя креатива (если нет других состояний) if u.Message.Text != "" { text := ui.FormatMessageHTML(u.Message) s.Text = &text s.DeleteUserMessage(b, u.Message.ID) s.UpdateCreativePreview(b) s.showManagementPanelWithDelete(b) } } func (s *CreativeDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return } func (s *CreativeDetails) Exit() {} func (s *CreativeDetails) showManagementPanelWithDelete(b *bot.Bot) { var confirmText, confirmCallback string if s.confirmingDelete { confirmText = "✖ Подтвердить удаление" confirmCallback = "confirm_delete" } else { confirmText = "✓ Сохранить" confirmCallback = "confirm_save" } // Добавляем кнопку удаления креатива deleteButton := Row(Button("⌦ Удалить креатив", "delete_creative")) s.ShowManagementPanel(b, confirmText, confirmCallback, "cancel", deleteButton) } func (s *CreativeDetails) updateCreative(b *bot.Bot) { // JWT уже создан в Bot.Update(), просто берем из сессии jwt := b.Session.JWT if jwt == "" { log.Error().Msg("JWT is empty in session") b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", Keyboard()) return } var err error // Подготавливаем данные для обновления input := backend.UpdateCreativeInput{ Name: s.Name, Text: s.Text, Tag: s.Tag, } if s.MediaChanged { if len(s.MediaItems) > 0 { mediaItems, err := s.BuildMediaInputs(b) if err != nil { log.Error().Err(err).Msg("Failed to download creative media") // Показываем ошибку через панель управления s.ShowControlPanel(b, "❌ Не удалось загрузить медиа\n\nПопробуйте удалить медиа и добавить заново, или нажмите \"Сохранить\" без изменения медиа.", [][]echotron.InlineKeyboardButton{ Row(Button("← Назад", "back")), Row(Button("✓ Сохранить без медиа", "confirm_save")), }) return } input.MediaItems = &mediaItems } else { emptyItems := []backend.CreativeMediaInput{} input.MediaItems = &emptyItems } } buttons := make([]backend.CreativeButton, 0, len(s.Buttons)) for _, button := range s.Buttons { buttons = append(buttons, backend.CreativeButton{ Text: button.Text, URL: button.URL, }) } input.Buttons = &buttons // Отправляем запрос на обновление _, err = b.Backend.UpdateCreative(context.Background(), jwt, b.Session.WorkspaceID, s.CreativeID, input) if err != nil { log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to update creative") // Показываем ошибку через панель управления s.ShowControlPanel(b, "❌ Не удалось сохранить изменения\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{ Row(Button("← Назад", "back")), Row(Button("✓ Попробовать снова", "confirm_save")), }) return } log.Info().Str("creative_id", s.CreativeID).Msg("Creative updated successfully") // Возвращаемся назад if s.ViewMode && s.ViewBackTo != nil { // Если открыты из view mode, возвращаемся к просмотру b.SetState(s.ViewBackTo, bot.EditMessage) } else if s.BackState != nil { // Иначе возвращаемся к BackState b.SetState(s.BackState, bot.EditMessage) } } func (s *CreativeDetails) deleteCreative(b *bot.Bot) { err := b.Backend.DeleteCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CreativeID) if err != nil { s.confirmingDelete = false // Сбрасываем флаг подтверждения s.ShowControlPanel(b, "❌ Не удалось удалить креатив\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{ Row(Button("← Назад", "back")), Row(Button("⌦ Попробовать снова", "delete_creative")), }) return } if s.BackState != nil { b.SetState(s.BackState, bot.EditMessage) } }