274 lines
8.4 KiB
Go
274 lines
8.4 KiB
Go
package screens
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/NicoNex/echotron/v3"
|
||
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
|
||
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||
)
|
||
|
||
type PlacementDetails struct {
|
||
ProjectID string
|
||
PlacementID string
|
||
BackState bot.State
|
||
}
|
||
|
||
func (s *PlacementDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||
s.renderDetails(b, mode)
|
||
}
|
||
|
||
func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
|
||
placement, err := b.Backend.GetPlacement(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, s.PlacementID)
|
||
if err != nil {
|
||
b.SendNew("❌ Не удалось загрузить размещение", Keyboard())
|
||
return
|
||
}
|
||
|
||
placementPost := placement.PlacementPost
|
||
|
||
text := "<b>Детали размещения</b>\n\n"
|
||
|
||
// Размещение в канале
|
||
text += "📺 <b>Размещение в канале:</b>\n"
|
||
text += formatChannelWithName(placement.Channel)
|
||
text += "\n"
|
||
|
||
// Рекламируемый проект
|
||
if placement.Project != nil {
|
||
text += "🎯 <b>Рекламируемый проект:</b>\n"
|
||
text += formatProject(*placement.Project)
|
||
text += "\n"
|
||
}
|
||
|
||
// Ссылка на пост
|
||
if placementPost != nil && placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
|
||
text += fmt.Sprintf("🔗 <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
|
||
}
|
||
|
||
// Пригласительная ссылка
|
||
if placement.InviteLink != nil && *placement.InviteLink != "" {
|
||
text += fmt.Sprintf("📩 <b>Пригласительная ссылка:</b> %s\n", *placement.InviteLink)
|
||
}
|
||
|
||
// Тип ссылки
|
||
text += fmt.Sprintf("🔓 <b>Тип ссылки:</b> %s\n", formatInviteLinkType(placement.InviteLinkType))
|
||
|
||
// Порядковый номер размещения
|
||
if placement.PlacementNumber > 0 {
|
||
text += fmt.Sprintf("🔢 <b>Размещение №%d</b> в этом канале\n", placement.PlacementNumber)
|
||
}
|
||
|
||
// Креатив
|
||
if placement.Details != nil && placement.Details.CreativeName != nil && *placement.Details.CreativeName != "" {
|
||
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.Details.CreativeName)
|
||
} else if placement.CreativeName != nil && *placement.CreativeName != "" {
|
||
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.CreativeName)
|
||
}
|
||
|
||
text += "\n"
|
||
|
||
// Финансовая информация
|
||
if placement.Details != nil {
|
||
// Стоимость
|
||
if placement.Details.Cost != nil {
|
||
costType := formatCostType(placement.Details.Cost.Type)
|
||
text += fmt.Sprintf("💰 <b>Стоимость:</b> %s %.0f₽\n", costType, placement.Details.Cost.Value)
|
||
}
|
||
|
||
// Стоимость до торга
|
||
if placement.Details.CostBeforeBargain != nil {
|
||
costType := formatCostType(placement.Details.CostBeforeBargain.Type)
|
||
text += fmt.Sprintf("💸 <b>До торга:</b> %s %.0f₽\n", costType, placement.Details.CostBeforeBargain.Value)
|
||
}
|
||
|
||
// Дата размещения
|
||
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
|
||
text += fmt.Sprintf("📅 <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
|
||
}
|
||
|
||
// Дата оплаты
|
||
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
|
||
text += fmt.Sprintf("💳 <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
|
||
}
|
||
|
||
// Формат
|
||
if placement.Details.Format != nil && *placement.Details.Format != "" {
|
||
text += fmt.Sprintf("📐 <b>Формат:</b> %s\n", *placement.Details.Format)
|
||
}
|
||
|
||
// Тип закупа
|
||
if placement.Details.PlacementType != nil {
|
||
text += fmt.Sprintf("🔄 <b>Тип закупа:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
|
||
}
|
||
}
|
||
|
||
text += "\n"
|
||
|
||
// Статистика
|
||
if placementPost != nil {
|
||
// Кол-во подписчиков
|
||
if placementPost.SubscriptionsCount > 0 {
|
||
text += fmt.Sprintf("👥 <b>Подписчики:</b> %d\n", placementPost.SubscriptionsCount)
|
||
}
|
||
|
||
// Просмотры поста
|
||
if placementPost.ViewsCount != nil {
|
||
text += fmt.Sprintf("👁️ <b>Просмотры:</b> %d\n", *placementPost.ViewsCount)
|
||
}
|
||
|
||
// CPF (стоимость подписчика)
|
||
if placementPost.SubscriptionsCount > 0 && placement.Details != nil && placement.Details.Cost != nil {
|
||
cpf := calculateCPF(placement.Details.Cost.Value, placementPost.SubscriptionsCount)
|
||
text += fmt.Sprintf("💵 <b>CPF:</b> %.2f₽\n", cpf)
|
||
}
|
||
|
||
// Конверсия
|
||
if placementPost.ViewsCount != nil && *placementPost.ViewsCount > 0 && placementPost.SubscriptionsCount > 0 {
|
||
conversion := calculateConversion(placementPost.SubscriptionsCount, *placementPost.ViewsCount)
|
||
text += fmt.Sprintf("📊 <b>Конверсия:</b> %.1f%%\n", conversion)
|
||
}
|
||
}
|
||
|
||
// Комментарий
|
||
if placement.Details != nil && placement.Details.Comment != nil && *placement.Details.Comment != "" {
|
||
text += fmt.Sprintf("\n📝 <b>Комментарий:</b> %s\n", *placement.Details.Comment)
|
||
}
|
||
|
||
keyboard := Keyboard(Row(Button("← Назад", "back")))
|
||
b.Render(text, keyboard, mode)
|
||
}
|
||
|
||
func formatChannelWithName(channel backend.Channel) string {
|
||
var name string
|
||
if channel.Title != nil && *channel.Title != "" {
|
||
name = *channel.Title
|
||
} else if channel.Username != nil && *channel.Username != "" {
|
||
name = "@" + *channel.Username
|
||
} else {
|
||
name = "Без названия"
|
||
}
|
||
|
||
// Добавляем ссылку если есть username или invite_link
|
||
if channel.Username != nil && *channel.Username != "" {
|
||
return fmt.Sprintf(" %s (@%s)\n", name, *channel.Username)
|
||
} else if channel.InviteLink != nil && *channel.InviteLink != "" {
|
||
return fmt.Sprintf(" %s\n", name)
|
||
}
|
||
return fmt.Sprintf(" %s\n", name)
|
||
}
|
||
|
||
func formatProject(project backend.ProjectOutput) string {
|
||
var name string
|
||
if project.Channel.Title != nil && *project.Channel.Title != "" {
|
||
name = *project.Channel.Title
|
||
} else if project.Channel.Username != nil && *project.Channel.Username != "" {
|
||
name = "@" + *project.Channel.Username
|
||
} else {
|
||
name = "Без названия"
|
||
}
|
||
|
||
// Добавляем ссылку если есть username
|
||
if project.Channel.Username != nil && *project.Channel.Username != "" {
|
||
return fmt.Sprintf(" %s (@%s)\n", name, *project.Channel.Username)
|
||
}
|
||
return fmt.Sprintf(" %s\n", name)
|
||
}
|
||
|
||
func calculateCPF(cost float64, subscriptions int) float64 {
|
||
if subscriptions == 0 {
|
||
return 0
|
||
}
|
||
return cost / float64(subscriptions)
|
||
}
|
||
|
||
func calculateConversion(subscriptions int, views int) float64 {
|
||
if views == 0 {
|
||
return 0
|
||
}
|
||
return float64(subscriptions) / float64(views) * 100
|
||
}
|
||
|
||
func formatInviteLinkType(value string) string {
|
||
if value == "approval" {
|
||
return "с одобрением"
|
||
}
|
||
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 formatPlacementType(value string) string {
|
||
normalized := strings.TrimSpace(strings.ToLower(value))
|
||
switch normalized {
|
||
case "self_promo":
|
||
return "Самопиар"
|
||
case "standard":
|
||
return "Стандарт"
|
||
default:
|
||
return value
|
||
}
|
||
}
|
||
|
||
func formatCostType(value string) string {
|
||
switch strings.TrimSpace(strings.ToLower(value)) {
|
||
case "cpm":
|
||
return "СРМ"
|
||
default:
|
||
return "Фикс"
|
||
}
|
||
}
|
||
|
||
func formatDateTime(value string) string {
|
||
if value == "" {
|
||
return value
|
||
}
|
||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||
return parsed.Format("02.01.2006 15:04")
|
||
}
|
||
if parsed, err := time.Parse(time.RFC3339Nano, value); err == nil {
|
||
return parsed.Format("02.01.2006 15:04")
|
||
}
|
||
return value
|
||
}
|
||
|
||
func (s *PlacementDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||
return
|
||
}
|
||
|
||
switch u.CallbackQuery.Data {
|
||
case "back":
|
||
if s.BackState != nil {
|
||
b.SetState(s.BackState, bot.EditMessage)
|
||
}
|
||
default:
|
||
s.Enter(b, bot.NewMessage)
|
||
}
|
||
}
|
||
|
||
func (s *PlacementDetails) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||
|
||
func (s *PlacementDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||
|
||
func (s *PlacementDetails) Exit() {}
|