Files
tgex-backend/tg_bot/screens/placements.go
Artem Tsyrulnikov 4b9e5bf4df правки
2026-02-18 20:47:19 +03:00

247 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package screens
import (
"context"
"fmt"
"time"
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
)
type Placements struct {
ProjectID string
ProjectTitle string
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
BackState bot.State
}
const msgPlacements = `
<b>Размещения</b>
Управление размещениями для проекта.
`
func (s *Placements) Enter(b *bot.Bot, mode bot.RenderMode) {
var rows [][]echotron.InlineKeyboardButton
rows = append(rows, Row(Button(" Создать размещение", "add_purchase")))
rows = append(rows, Row(Button("Список размещений", "placements_list"), URLButton("План закупов", fmt.Sprintf("https://app.smart-post.ru/dashboard/%s/purchase-plans/%s", b.Session.WorkspaceID, s.ProjectID))))
rows = append(rows, Row(Button("← Назад", "back")))
keyboard := Keyboard(rows...)
b.Render(msgPlacements, keyboard, mode)
updateProjectHeaderMedia(b, b.LastMessageID, msgPlacements, keyboard, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus)
}
func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch data {
case "back":
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
}
case "add_purchase":
b.SetState(&AddPurchase{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectTelegramID: s.ProjectTelegramID,
ProjectUsername: s.ProjectUsername,
ProjectStatus: s.ProjectStatus,
ActivePicker: "",
BackState: s,
}, bot.EditMessage)
case "placements_list":
b.SetState(&PlacementsList{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectTelegramID: s.ProjectTelegramID,
ProjectUsername: s.ProjectUsername,
ProjectStatus: s.ProjectStatus,
BackState: s,
}, bot.EditMessage)
default:
s.Enter(b, bot.NewMessage)
}
}
func (s *Placements) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *Placements) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *Placements) Exit() {}
// PlacementsList - экран со списком размещений
const placementsPerPage = 6
const placementsPerRow = 1
type PlacementsList struct {
ProjectID string
ProjectTitle string
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
CurrentPage int
BackState bot.State
}
func (s *PlacementsList) Enter(b *bot.Bot, mode bot.RenderMode) {
s.render(b, mode)
}
func (s *PlacementsList) render(b *bot.Bot, mode bot.RenderMode) {
page, err := b.Backend.GetPlacements(
context.Background(),
b.Session.JWT,
b.Session.WorkspaceID,
s.ProjectID,
s.CurrentPage+1,
placementsPerPage,
)
if err != nil {
b.SendNew("❌ Не удалось загрузить размещения", Keyboard())
return
}
text := fmt.Sprintf("<b>Список размещений</b>%s\n\n", ui.FormatPageInfo(s.CurrentPage, page.Pages))
var rows [][]echotron.InlineKeyboardButton
if len(page.Items) == 0 {
text += `Список размещений пуст.
<i>Размещение — это план публикации рекламы в канале:</i>
‣ Выбор креатива
‣ Канал размещения
‣ Стоимость и формат
‣ Статус выполнения
Создайте первое размещение`
} else {
// Формируем кнопки для каждого размещения
buttons := make([]echotron.InlineKeyboardButton, 0, len(page.Items))
for i, placement := range page.Items {
// Нумерация по порядку создания: первое размещение = #1 (на последней странице),
// последнее размещение = #N (вверху).
globalNum := page.Total - (s.CurrentPage * placementsPerPage) - i
// Название канала
channelName := "Без названия"
if placement.Channel.Title != nil && *placement.Channel.Title != "" {
channelName = *placement.Channel.Title
} else if placement.Channel.Username != nil && *placement.Channel.Username != "" {
channelName = "@" + *placement.Channel.Username
}
// Дата размещения
dateStr := ""
if placement.Details != nil && placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
dateStr = formatDate(*placement.Details.PlacementAt)
}
// Текст кнопки: #N • Канал • DD.MM
buttonText := fmt.Sprintf("#%d • %s", globalNum, channelName)
if dateStr != "" {
buttonText += fmt.Sprintf(" • %s", dateStr)
}
buttons = append(buttons, echotron.InlineKeyboardButton{
Text: buttonText,
CallbackData: fmt.Sprintf("placement:%s", placement.ID),
})
}
// Раскладываем по сетке
rows = ui.BuildPageRows(buttons, placementsPerRow, placementsPerPage, page.Pages)
}
// Навигация (без MiddleButtons - без "Создать размещение")
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
CurrentPage: s.CurrentPage,
TotalPages: page.Pages,
}); navRow != nil {
rows = append(rows, navRow)
}
// Кнопка назад
rows = append(rows, Row(Button("← Назад", "back")))
keyboard := Keyboard(rows...)
b.Render(text, keyboard, mode)
}
func (s *PlacementsList) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch data {
case "back":
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
}
case "prev":
if s.CurrentPage > 0 {
s.CurrentPage--
}
s.Enter(b, bot.EditMessage)
case "next":
s.CurrentPage++
s.Enter(b, bot.EditMessage)
default:
// Проверяем, не нажали ли на конкретное размещение
if len(data) > 10 && data[:10] == "placement:" {
placementID := data[10:]
b.SetState(&PlacementDetails{
ProjectID: s.ProjectID,
PlacementID: placementID,
BackState: s,
}, bot.EditMessage)
} else {
s.Enter(b, bot.NewMessage)
}
}
}
func (s *PlacementsList) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *PlacementsList) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *PlacementsList) Exit() {}
// formatDate парсит ISO дату и форматирует как "27.01"
func formatDate(isoDate string) string {
// Пробуем парсить RFC3339
t, err := time.Parse(time.RFC3339, isoDate)
if err != nil {
// Пробуем другие форматы
t, err = time.ParseInLocation("2006-01-02T15:04:05", isoDate, ui.MskLocation)
if err != nil {
// Иногда бэкенд может вернуть только дату без времени
t, err = time.ParseInLocation("2006-01-02", isoDate, ui.MskLocation)
if err != nil {
return ""
}
}
}
return t.In(ui.MskLocation).Format("02.01")
}