@@ -0,0 +1,322 @@
|
||||
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 := "<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))
|
||||
|
||||
// Short ID
|
||||
if placement.ShortID != "" {
|
||||
text += fmt.Sprintf("🔢 <b>ID:</b> %s\n", placement.ShortID)
|
||||
}
|
||||
|
||||
// Креатив
|
||||
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
|
||||
}
|
||||
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() {}
|
||||
Reference in New Issue
Block a user