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 := "Детали размещения\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) } if parsed, err = time.Parse(time.RFC3339Nano, value); err == nil { return formatCompactDateTime(parsed) } if parsed, err = time.Parse("2006-01-02T15:04:05", value); err == nil { return formatCompactDateTime(parsed) } if parsed, err = time.Parse("2006-01-02", value); err == nil { return formatCompactDateTime(parsed) } 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() {}