выбор времени

This commit is contained in:
Artem Tsyrulnikov
2026-01-03 12:41:33 +03:00
parent ab9af29f24
commit 841a8b5aaa
10 changed files with 1103 additions and 132 deletions

View File

@@ -3,6 +3,7 @@ package screens
import (
"context"
"fmt"
"regexp"
"strings"
"example.com/m/adapter/backend"
@@ -18,14 +19,19 @@ type PurchaseChannelInput struct {
}
type SelectChannelsForPurchase struct {
WorkspaceID string
ProjectID string
ProjectTitle string
CreativeID string
Channels []PurchaseChannelInput
BackState bot.State
WorkspaceID string
ProjectID string
ProjectTitle string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
CurrentPage int
InvalidUsernames []string
BackState bot.State
}
var channelUsernameRe = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{4,31}$`)
func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
s.renderChannelSelection(b, mode)
}
@@ -43,7 +49,7 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
Отправьте username канала (без @)
Например: <code>channel_name</code>
После добавления всех каналов нажмите <b>Создать закуп</b>`
После добавления всех каналов нажмите <b>Далее</b>`
} else {
text += fmt.Sprintf("Добавлено каналов: <b>%d</b>\n\n", len(s.Channels))
@@ -54,19 +60,83 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
}
text += channelText + "\n"
}
text += "\n"
const channelsPerPage = 6
start := s.CurrentPage * channelsPerPage
if start >= len(s.Channels) {
s.CurrentPage = 0
start = 0
}
end := start + channelsPerPage
if end > len(s.Channels) {
end = len(s.Channels)
}
var slots []echotron.InlineKeyboardButton
for i := start; i < end; i++ {
ch := s.Channels[i]
buttonText := fmt.Sprintf("@%s", ch.Username)
if ch.PlannedCost != nil {
buttonText = fmt.Sprintf("@%s — %.0f₽", ch.Username, *ch.PlannedCost)
}
slots = append(slots, bot.Button(buttonText, fmt.Sprintf("remove_channel:%d", i)))
}
if len(s.Channels) > channelsPerPage {
for len(slots) < channelsPerPage {
slots = append(slots, bot.Button(" ", "empty"))
}
}
for i := 0; i < len(slots); i += 2 {
row := []echotron.InlineKeyboardButton{slots[i]}
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
row = append(row, bot.Button(" ", "empty"))
}
buttons = append(buttons, row)
}
pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage
if pages > 1 {
var navRow []echotron.InlineKeyboardButton
if s.CurrentPage > 0 {
navRow = append(navRow, bot.Button("←", "prev_page"))
} else {
navRow = append(navRow, bot.Button("·", "empty"))
}
navRow = append(navRow, bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, pages), "page_info"))
if s.CurrentPage+1 < pages {
navRow = append(navRow, bot.Button("→", "next_page"))
} else {
navRow = append(navRow, bot.Button("·", "empty"))
}
buttons = append(buttons, navRow)
}
text += "\nДобавьте еще каналы или создайте закуп"
// Кнопка создания закупа (доступна только если есть каналы)
buttons = append(buttons, bot.Row(
bot.Button("● Создать закуп", "create"),
))
}
if len(s.InvalidUsernames) > 0 {
text += fmt.Sprintf("\n\n⚠ Пропущены: %s", strings.Join(s.InvalidUsernames, ", "))
s.InvalidUsernames = nil
}
// Кнопки навигации
buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"),
))
if len(s.Channels) > 0 {
buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"),
bot.Button("→ Далее", "next"),
))
} else {
buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"),
))
}
keyboard := bot.Keyboard(buttons...)
b.Render(text, keyboard, mode)
@@ -97,13 +167,16 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
case "back":
// Возвращаемся к выбору креатива
b.SetState(&AddPurchase{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
BackState: s.BackState,
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
ActivePicker: "",
BackState: s.BackState,
}, bot.EditMessage)
case "create":
case "next":
if len(s.Channels) == 0 {
b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")),
@@ -111,20 +184,47 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
return
}
jwt, err := s.ensureJWT(b)
if err != nil {
log.Error().Err(err).Msg("Failed to get JWT")
b.Edit("❌ Ошибка авторизации", bot.Keyboard())
return
}
// Создаем закуп
s.createPurchase(b, jwt)
b.SetState(&PurchaseOptionalDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
BackState: s.BackState,
}, bot.EditMessage)
case "back_to_select":
s.Enter(b, bot.EditMessage)
case "prev_page":
if s.CurrentPage > 0 {
s.CurrentPage--
}
s.Enter(b, bot.EditMessage)
case "next_page":
s.CurrentPage++
s.Enter(b, bot.EditMessage)
default:
if len(data) > 15 && data[:15] == "remove_channel:" {
var index int
if _, err := fmt.Sscanf(data[15:], "%d", &index); err == nil {
if index >= 0 && index < len(s.Channels) {
s.Channels = append(s.Channels[:index], s.Channels[index+1:]...)
if s.CurrentPage > 0 {
channelsPerPage := 6
pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage
if s.CurrentPage >= pages {
s.CurrentPage = pages - 1
}
}
s.Enter(b, bot.EditMessage)
return
}
}
}
s.Enter(b, bot.NewMessage)
}
}
@@ -182,27 +282,55 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
return
}
username := strings.TrimSpace(u.Message.Text)
// Убираем @ если пользователь его добавил
username = strings.TrimPrefix(username, "@")
if username == "" {
raw := strings.TrimSpace(u.Message.Text)
if raw == "" {
return
}
// Проверяем, не добавлен ли уже этот канал
for _, ch := range s.Channels {
if ch.Username == username {
b.SendNew(fmt.Sprintf("⚠️ Канал @%s уже добавлен в список", username), bot.Keyboard())
tokens := strings.FieldsFunc(raw, func(r rune) bool {
return r == ' ' || r == '\n' || r == '\t' || r == ',' || r == ';'
})
added := 0
var invalid []string
for _, token := range tokens {
username := strings.TrimSpace(token)
username = strings.TrimPrefix(username, "@")
username = strings.Trim(username, "@")
username = strings.Trim(username, ",;")
if username == "" {
continue
}
if !channelUsernameRe.MatchString(username) {
invalid = append(invalid, username)
continue
}
exists := false
for _, ch := range s.Channels {
if ch.Username == username {
exists = true
break
}
}
if exists {
continue
}
s.Channels = append(s.Channels, PurchaseChannelInput{
Username: username,
})
added++
}
if added == 0 {
if len(invalid) == 0 {
return
}
}
// Добавляем канал в список
s.Channels = append(s.Channels, PurchaseChannelInput{
Username: username,
})
s.InvalidUsernames = invalid
// Обновляем экран
s.Enter(b, bot.NewMessage)