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"
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
)
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 := "Детали размещения\n\n"
// Размещение в канале
text += "📺 Размещение в канале:\n"
text += formatChannelWithName(placement.Channel)
text += "\n"
// Рекламируемый проект
if placement.Project != nil {
text += "🎯 Рекламируемый проект:\n"
text += formatProject(*placement.Project)
text += "\n"
}
// Ссылка на пост
if placementPost != nil && placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
text += fmt.Sprintf("🔗 Ссылка на пост: %s\n", *placementPost.Post.URL)
}
// Пригласительная ссылка
if placement.InviteLink != nil && *placement.InviteLink != "" {
text += fmt.Sprintf("📩 Пригласительная ссылка: %s\n", *placement.InviteLink)
}
// Тип ссылки
text += fmt.Sprintf("🔓 Тип ссылки: %s\n", formatInviteLinkType(placement.InviteLinkType))
// Short ID
if placement.ShortID != "" {
text += fmt.Sprintf("🔢 ID: %s\n", placement.ShortID)
}
// Креатив
if placement.Details != nil && placement.Details.CreativeName != nil && *placement.Details.CreativeName != "" {
text += fmt.Sprintf("✨ Креатив: %s\n", *placement.Details.CreativeName)
} else if placement.CreativeName != nil && *placement.CreativeName != "" {
text += fmt.Sprintf("✨ Креатив: %s\n", *placement.CreativeName)
}
text += "\n"
// Финансовая информация
if placement.Details != nil {
// Стоимость
if placement.Details.Cost != nil {
costType := formatCostType(placement.Details.Cost.Type)
text += fmt.Sprintf("💰 Стоимость: %s %.0f₽\n", costType, placement.Details.Cost.Value)
}
// Стоимость до торга
if placement.Details.CostBeforeBargain != nil {
costType := formatCostType(placement.Details.CostBeforeBargain.Type)
text += fmt.Sprintf("💸 До торга: %s %.0f₽\n", costType, placement.Details.CostBeforeBargain.Value)
}
// Дата размещения
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
text += fmt.Sprintf("📅 Дата размещения: %s\n", formatDateTime(*placement.Details.PlacementAt))
}
// Дата оплаты
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
text += fmt.Sprintf("💳 Дата оплаты: %s\n", formatDateTime(*placement.Details.PaymentAt))
}
// Формат
if placement.Details.Format != nil && *placement.Details.Format != "" {
text += fmt.Sprintf("📐 Формат: %s\n", *placement.Details.Format)
}
// Тип закупа
if placement.Details.PlacementType != nil {
text += fmt.Sprintf("🔄 Тип закупа: %s\n", formatPlacementType(*placement.Details.PlacementType))
}
}
text += "\n"
// Статистика
if placementPost != nil {
// Кол-во подписчиков
if placementPost.SubscriptionsCount > 0 {
text += fmt.Sprintf("👥 Подписчики: %d\n", placementPost.SubscriptionsCount)
}
// Просмотры поста
if placementPost.ViewsCount != nil {
text += fmt.Sprintf("👁️ Просмотры: %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("💵 CPF: %.2f₽\n", cpf)
}
// Конверсия
if placementPost.ViewsCount != nil && *placementPost.ViewsCount > 0 && placementPost.SubscriptionsCount > 0 {
conversion := calculateConversion(placementPost.SubscriptionsCount, *placementPost.ViewsCount)
text += fmt.Sprintf("📊 Конверсия: %.1f%%\n", conversion)
}
}
// Комментарий
if placement.Details != nil && placement.Details.Comment != nil && *placement.Details.Comment != "" {
text += fmt.Sprintf("\n📝 Комментарий: %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
}
var parsed time.Time
var err error
// Try parsing with timezone
if parsed, err = time.Parse(time.RFC3339, value); err == nil {
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
if parsed, err = time.Parse(time.RFC3339Nano, value); err == nil {
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
if parsed, err = time.ParseInLocation("2006-01-02T15:04:05", value, ui.MskLocation); err == nil {
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
if parsed, err = time.ParseInLocation("2006-01-02", value, ui.MskLocation); err == nil {
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
return value
}
func WeekdayName(day time.Weekday) string {
switch day {
case time.Monday:
return "Пн"
case time.Tuesday:
return "Вт"
case time.Wednesday:
return "Ср"
case time.Thursday:
return "Чт"
case time.Friday:
return "Пт"
case time.Saturday:
return "Сб"
case time.Sunday:
return "Вс"
default:
return ""
}
}
func MonthShort(month time.Month) string {
months := []string{
"янв", "фев", "мар", "апр", "май", "июн",
"июл", "авг", "сен", "окт", "ноя", "дек",
}
if int(month) < 1 || int(month) > len(months) {
return ""
}
return months[int(month)-1]
}
func formatCompactDateTime(value time.Time) string {
weekday := WeekdayName(value.Weekday())
month := MonthShort(value.Month())
return fmt.Sprintf("%s %02d %s %s", weekday, value.Day(), month, value.Format("15:04"))
}
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() {}