Files
tgex-backend/tg_bot/screens/purchase_details.go
Artem Tsyrulnikov 62e8f98429 refactor
2026-01-18 14:10:13 +03:00

274 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package screens
import (
"context"
"fmt"
"strings"
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
"github.com/rs/zerolog/log"
)
type PurchaseDetails struct {
WorkspaceID string
ProjectID string
PurchaseID string
BackState bot.State
}
func (s *PurchaseDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
return
}
s.renderDetails(b, mode, jwt)
}
func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt string) {
purchase, err := b.Backend.GetPurchase(
context.Background(),
jwt,
s.WorkspaceID,
s.ProjectID,
s.PurchaseID,
)
if err != nil {
log.Error().Err(err).Msg("Failed to get purchase")
b.SendNew("❌ Не удалось загрузить закуп", Keyboard())
return
}
placementsByChannel := s.loadPlacementsByChannel(b, jwt, purchase)
// Получаем информацию о креативе
creative, err := b.Backend.GetCreative(
context.Background(),
jwt,
s.WorkspaceID,
purchase.CreativeID,
)
if err != nil {
log.Error().Err(err).Msg("Failed to get creative")
creative = nil
}
totalPlacements := 0
for _, items := range placementsByChannel {
totalPlacements += len(items)
}
text := "<b>Детали закупа</b>\n\n"
// Информация о креативе
if creative != nil {
text += fmt.Sprintf("• <b>Креатив:</b> %s\n", creative.Name)
} else {
text += fmt.Sprintf("• <b>Креатив ID:</b> %s\n", purchase.CreativeID)
}
// Статус закупа
statusSymbol := "●"
statusText := "Активный"
if purchase.Status == "archived" {
statusSymbol = "■"
statusText = "Архивирован"
}
text += fmt.Sprintf("• <b>Статус:</b> %s %s\n", statusSymbol, statusText)
text += fmt.Sprintf("• <b>Каналов:</b> %d\n", len(purchase.Channels))
text += fmt.Sprintf("• <b>Размещений:</b> %d\n\n", totalPlacements)
// Информация о каналах
if len(purchase.Channels) == 0 {
text += "<i>Каналы не добавлены</i>\n"
} else {
text += fmt.Sprintf("<b>Каналы (%d):</b>\n\n", len(purchase.Channels))
for i, ch := range purchase.Channels {
chStatusText := formatPurchaseChannelStatus(ch.Status)
// Название канала
channelName := "Без названия"
if ch.Channel.Title != nil {
channelName = *ch.Channel.Title
} else if ch.Channel.Username != nil {
channelName = "@" + *ch.Channel.Username
}
text += fmt.Sprintf("%d. <b>%s</b>\n", i+1, channelName)
text += fmt.Sprintf(" ◦ <i>%s</i>\n", chStatusText)
// Стоимость
if ch.Details != nil && ch.Details.Cost != nil {
text += fmt.Sprintf(" ◦ Стоимость: %.0f₽\n", ch.Details.Cost.Value)
}
// Дата размещения (план)
if ch.Details != nil && ch.Details.PlacementAt != nil && *ch.Details.PlacementAt != "" {
text += fmt.Sprintf(" ◦ План: %s\n", *ch.Details.PlacementAt)
}
// Комментарий
if ch.Comment != nil && *ch.Comment != "" {
text += fmt.Sprintf(" ◦ %s\n", *ch.Comment)
}
// Информация о размещении
placements := placementsByChannel[ch.ID]
if len(placements) == 0 {
text += " ◦ Размещение: <b>нет</b>\n"
} else {
placement := placements[0]
text += fmt.Sprintf(" ◦ Размещение: <b>есть</b> (%d)\n", len(placements))
if placement.Status != "" {
text += fmt.Sprintf(" ◦ Статус размещения: %s\n", formatPlacementStatus(placement.Status))
}
if placement.PlacementDate != "" {
text += fmt.Sprintf(" ◦ Дата: %s\n", placement.PlacementDate)
}
if placement.AdPostURL != nil && *placement.AdPostURL != "" {
text += fmt.Sprintf(" ◦ Пост: %s\n", *placement.AdPostURL)
}
if placement.SubscriptionsCount > 0 {
text += fmt.Sprintf(" ◦ Подписки: %d\n", placement.SubscriptionsCount)
}
}
// Тип invite link
inviteLinkTypeText := "публичная"
if ch.InviteLinkType == "approval" {
inviteLinkTypeText = "с одобрением"
}
text += fmt.Sprintf(" ◦ Ссылка: %s\n", inviteLinkTypeText)
text += "\n"
}
}
var buttons [][]echotron.InlineKeyboardButton
// Кнопка назад
buttons = append(buttons, Row(
Button("← Назад", "back"),
))
keyboard := Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
func (s *PurchaseDetails) loadPlacementsByChannel(
b *bot.Bot,
jwt string,
purchase *backend.Purchase,
) map[string][]backend.Placement {
placementsByChannel := make(map[string][]backend.Placement)
if purchase == nil || purchase.CreativeID == "" || len(purchase.Channels) == 0 {
return placementsByChannel
}
projectID := s.ProjectID
creativeID := purchase.CreativeID
pageSize := 50
maxPages := 5
foundChannels := make(map[string]bool)
for page := 1; page <= maxPages; page++ {
resp, err := b.Backend.GetPlacements(
context.Background(),
jwt,
s.WorkspaceID,
&projectID,
nil,
&creativeID,
false,
page,
pageSize,
)
if err != nil {
log.Error().Err(err).Msg("Failed to get placements for purchase details")
break
}
for _, placement := range resp.Items {
if placement.PurchaseID == nil || *placement.PurchaseID != purchase.ID {
continue
}
if placement.PurchaseChannelID == nil || *placement.PurchaseChannelID == "" {
continue
}
chID := *placement.PurchaseChannelID
placementsByChannel[chID] = append(placementsByChannel[chID], placement)
foundChannels[chID] = true
}
if resp.Pages <= page || len(foundChannels) == len(purchase.Channels) {
break
}
}
return placementsByChannel
}
func formatPurchaseChannelStatus(status string) string {
switch status {
case "planned":
return "Планируется"
case "approved":
return "Согласовано"
case "rejected":
return "Отклонено"
case "in_progress":
return "В работе"
case "completed":
return "Завершено"
default:
return "Неизвестно"
}
}
func formatPlacementStatus(status string) string {
normalized := strings.TrimSpace(strings.ToLower(status))
switch normalized {
case "planned":
return "Планируется"
case "approved":
return "Согласовано"
case "rejected":
return "Отклонено"
case "in_progress":
return "В работе"
case "completed":
return "Размещено"
default:
return status
}
}
func (s *PurchaseDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch data {
case "back":
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
}
default:
s.Enter(b, bot.NewMessage)
}
}
func (s *PurchaseDetails) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *PurchaseDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *PurchaseDetails) Exit() {}