домен обновлен

This commit is contained in:
Artem Tsyrulnikov
2026-01-07 15:11:28 +03:00
parent f6669c3f5a
commit e4199fc9c9
30 changed files with 696 additions and 1201 deletions

View File

@@ -3,7 +3,9 @@ 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"
@@ -42,6 +44,8 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
return
}
placementsByChannel := s.loadPlacementsByChannel(b, jwt, purchase)
// Получаем информацию о креативе
creative, err := b.Backend.GetCreative(
context.Background(),
@@ -54,13 +58,18 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
creative = nil
}
totalPlacements := 0
for _, items := range placementsByChannel {
totalPlacements += len(items)
}
text := "<i>… Закупы Детали</i>\n\n<b>Детали закупа</b>\n\n"
// Информация о креативе
if creative != nil {
text += fmt.Sprintf(" <b>Креатив:</b> %s\n", creative.Name)
text += fmt.Sprintf(" <b>Креатив:</b> %s\n", creative.Name)
} else {
text += fmt.Sprintf(" <b>Креатив ID:</b> %s\n", purchase.CreativeID)
text += fmt.Sprintf(" <b>Креатив ID:</b> %s\n", purchase.CreativeID)
}
// Статус закупа
@@ -70,7 +79,9 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
statusSymbol = "■"
statusText = "Архивирован"
}
text += fmt.Sprintf(" <b>Статус:</b> %s %s\n\n", 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 {
@@ -79,20 +90,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
text += fmt.Sprintf("<b>Каналы (%d):</b>\n\n", len(purchase.Channels))
for i, ch := range purchase.Channels {
// Определяем статус
statusSymbol := "○"
switch ch.Status {
case "planned":
statusSymbol = "○"
case "approved":
statusSymbol = "◉"
case "rejected":
statusSymbol = "⊗"
case "in_progress":
statusSymbol = "◐"
case "completed":
statusSymbol = "●"
}
chStatusText := formatPurchaseChannelStatus(ch.Status)
// Название канала
channelName := "Без названия"
@@ -102,16 +100,43 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
channelName = "@" + *ch.Channel.Username
}
text += fmt.Sprintf("%d. %s <b>%s</b>\n", i+1, statusSymbol, channelName)
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)
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)
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
@@ -119,7 +144,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"
}
@@ -136,6 +161,94 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
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

View File

@@ -1283,9 +1283,13 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
}
}
b.SendNew("✅ Закуп создан", bot.Keyboard(
bot.Row(bot.Button("● Готово", "done")),
))
b.SetState(&PurchaseDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
PurchaseID: purchase.ID,
BackState: s.BackState,
}, bot.NewMessage)
}
func (s *PurchaseOptionalDetails) buildPurchaseDetails(purchaseType *string) *backend.PurchaseDetails {
@@ -1449,5 +1453,15 @@ func (s *PurchaseOptionalDetails) injectInviteLink(text, inviteLink string) stri
return text
}
re := regexp.MustCompile(`<a\s+class="tg-link">([^<]*)</a>`)
return re.ReplaceAllString(text, fmt.Sprintf(`<a href="%s">$1</a>`, inviteLink))
return re.ReplaceAllStringFunc(text, func(match string) string {
sub := re.FindStringSubmatch(match)
if len(sub) < 2 {
return match
}
inner := strings.TrimSpace(sub[1])
if inner == "" {
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLink, inviteLink)
}
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLink, sub[1])
})
}

View File

@@ -245,22 +245,13 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
return
}
// Показываем успешное создание
successText := fmt.Sprintf(`
<i>… Создание Готово</i>
<b>Закуп создан</b>
▪ <b>ID:</b> %s
▪ <b>Креатив:</b> %s
▪ <b>Каналов:</b> %d
Закуп создан и готов к работе
`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
b.Edit(successText, bot.Keyboard(
bot.Row(bot.Button("● Готово", "done")),
))
b.SetState(&PurchaseDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
PurchaseID: purchase.ID,
BackState: s.BackState,
}, bot.EditMessage)
}
func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update) {