package screens import ( "context" "fmt" "strings" "example.com/m/adapter/backend" "example.com/m/bot" "github.com/NicoNex/echotron/v3" "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 := "Детали закупа\n\n" // Информация о креативе if creative != nil { text += fmt.Sprintf("• Креатив: %s\n", creative.Name) } else { text += fmt.Sprintf("• Креатив ID: %s\n", purchase.CreativeID) } // Статус закупа statusSymbol := "●" statusText := "Активный" if purchase.Status == "archived" { statusSymbol = "■" statusText = "Архивирован" } text += fmt.Sprintf("• Статус: %s %s\n", statusSymbol, statusText) text += fmt.Sprintf("• Каналов: %d\n", len(purchase.Channels)) text += fmt.Sprintf("• Размещений: %d\n\n", totalPlacements) // Информация о каналах if len(purchase.Channels) == 0 { text += "Каналы не добавлены\n" } else { text += fmt.Sprintf("Каналы (%d):\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. %s\n", i+1, channelName) text += fmt.Sprintf(" ◦ %s\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 += " ◦ Размещение: нет\n" } else { placement := placements[0] text += fmt.Sprintf(" ◦ Размещение: есть (%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() {}