This commit is contained in:
Artem Tsyrulnikov
2026-02-03 22:20:56 +03:00
parent 7c8589e000
commit 09145e510a
10 changed files with 124 additions and 101 deletions

View File

@@ -15,26 +15,28 @@ const addPurchaseCreativesPerPage = 5
const addPurchaseProjectsPerPage = 6
type AddPurchase struct {
ProjectID string
ProjectTitle string
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
CreativeID string
CreativeTitle string
ProjectPage int
CreativePage int
ActivePicker string // "" | "project_list" | "creative_list"
BackState bot.State
lastProjects map[string]projectInfo
lastCreatives map[string]creativeInfo
ProjectID string
ProjectTitle string
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
ProjectDefaultLinkType string
CreativeID string
CreativeTitle string
ProjectPage int
CreativePage int
ActivePicker string // "" | "project_list" | "creative_list"
BackState bot.State
lastProjects map[string]projectInfo
lastCreatives map[string]creativeInfo
}
type projectInfo struct {
Title string
TelegramID int64
Username string
Status string
Title string
TelegramID int64
Username string
Status string
DefaultInviteType string
}
type creativeInfo struct {
@@ -85,10 +87,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) {
username = *project.Username
}
s.lastProjects[project.ID] = projectInfo{
Title: project.Title,
TelegramID: project.TelegramID,
Username: username,
Status: project.Status,
Title: project.Title,
TelegramID: project.TelegramID,
Username: username,
Status: project.Status,
DefaultInviteType: project.PurchaseInviteTypeDefault,
}
}
@@ -263,17 +266,18 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
}
if s.ProjectID != "" && s.CreativeID != "" {
b.SetState(&SelectChannelsForPurchase{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectTelegramID: s.ProjectTelegramID,
ProjectUsername: s.ProjectUsername,
ProjectStatus: s.ProjectStatus,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: []PurchaseChannelInput{},
Duplicates: []string{},
ParsingErrors: []ParseError{},
BackState: s.BackState,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectTelegramID: s.ProjectTelegramID,
ProjectUsername: s.ProjectUsername,
ProjectStatus: s.ProjectStatus,
ProjectDefaultLinkType: s.ProjectDefaultLinkType,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: []PurchaseChannelInput{},
Duplicates: []string{},
ParsingErrors: []ParseError{},
BackState: s.BackState,
}, bot.EditMessage)
}
@@ -286,6 +290,7 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.ProjectTelegramID = info.TelegramID
s.ProjectUsername = info.Username
s.ProjectStatus = info.Status
s.ProjectDefaultLinkType = info.DefaultInviteType
}
}
s.ProjectID = projectID

View File

@@ -33,8 +33,9 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) {
kb := Keyboard(Row(URLButton("Войти на сайт", loginURL)))
res, err := b.SendMessage(fmt.Sprintf(msgHelloLogin, usernameDisplay), b.ChatID, &echotron.MessageOptions{
ReplyMarkup: kb,
ParseMode: echotron.HTML,
ReplyMarkup: kb,
ParseMode: echotron.HTML,
LinkPreviewOptions: echotron.LinkPreviewOptions{IsDisabled: true},
})
if err != nil {
log.Err(err).Msg("SendMessage error")

View File

@@ -14,7 +14,7 @@ type MainMenu struct{}
const msgAbout = `
<i>Главное меню</i>
<blockquote>
<blockquote expandable>
За что отвечает каждая кнопка в этом окне:
<b>• Рабочее пространство</b> — Одно можно использовать для своих проектов, а другое, например, для проектов, где вы являетесь закупщиком.

View File

@@ -29,8 +29,7 @@ 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")))
rows = append(rows, Row(URLButton("План закупов", fmt.Sprintf("https://app.smart-post.ru/dashboard/%s/purchase-plans/%s", b.Session.WorkspaceID, s.ProjectID))))
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...)

View File

@@ -17,6 +17,7 @@ import (
type PurchaseOptionalDetails struct {
ProjectID string
ProjectTitle string
ProjectDefaultLinkType string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
@@ -254,15 +255,16 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
case "back":
// Сохраняем текущее состояние перед возвратом назад
b.SetState(&SelectChannelsForPurchase{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
Duplicates: []string{},
ParsingErrors: []ParseError{},
OptionalDetailsState: s, // Сохраняем текущее состояние
BackState: s.BackState,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectDefaultLinkType: s.ProjectDefaultLinkType,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
Duplicates: []string{},
ParsingErrors: []ParseError{},
OptionalDetailsState: s, // Сохраняем текущее состояние
BackState: s.BackState,
}, bot.EditMessage)
case "next":
jwt := b.Session.JWT
@@ -314,11 +316,6 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
}
if strings.HasPrefix(u.CallbackQuery.Data, "type:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "type:")
if value == "custom" {
s.InputMode = "type_custom"
s.Enter(b, bot.EditMessage)
return
}
// Нормализуем значение для БД
switch value {
case "self_promo":
@@ -372,12 +369,6 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
}
switch s.InputMode {
case "type_custom":
if s.CurrentChannel != "" {
s.PurchaseTypeByChannel[s.CurrentChannel] = text
} else {
s.PurchaseType = text
}
case "format_custom":
s.setFormatValue(text)
case "comment":
@@ -639,11 +630,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
case "type":
text += "<b>Выберите тип закупа</b>"
keyboard := Keyboard(
Row(
Button("Самопиар", "type:self_promo"),
Button("Стандарт", "type:standard"),
),
Row(Button("Другое", "type:custom")),
Row(Button("Самопиар", "type:self_promo"), Button("Стандарт", "type:standard")),
Row(Button("← Назад", "back_to_optional")),
)
if mode == bot.EditMessage {
@@ -652,8 +639,6 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
b.SendNew(text, keyboard)
}
return
case "type_custom":
text += "<b>Введите тип закупа</b>\n\nНапример: <code>вп</code>"
case "cost_value":
text += "<b>Ввод стоимости</b>\n\nНапример: <code>15000</code>"
if s.CurrentChannel != "" {
@@ -1639,6 +1624,12 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.InviteLinkTypeMode == "" {
s.InviteLinkTypeMode = "common"
}
if s.InviteLinkType == "" {
s.InviteLinkType = s.ProjectDefaultLinkType
if s.InviteLinkType == "" {
s.InviteLinkType = "approval"
}
}
if s.PlacementByChannel == nil {
s.PlacementByChannel = make(map[string]*time.Time)
}
@@ -2255,22 +2246,22 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string, placeme
// Placement date
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
if value, ok := s.PlacementByChannel[channelKey]; ok && value != nil {
formatted := value.Format(time.RFC3339)
formatted := value.UTC().Format(time.RFC3339)
details.PlacementAt = &formatted
}
} else if s.PlacementDateTime != nil {
formatted := s.PlacementDateTime.Format(time.RFC3339)
formatted := s.PlacementDateTime.UTC().Format(time.RFC3339)
details.PlacementAt = &formatted
}
// Payment date
if s.PaymentDateMode == "per_channel" && len(s.Channels) > 1 {
if value, ok := s.PaymentDateByChannel[channelKey]; ok && value != nil {
formatted := value.Format(time.RFC3339)
formatted := value.UTC().Format(time.RFC3339)
details.PaymentAt = &formatted
}
} else if s.PaymentDate != nil {
formatted := s.PaymentDate.Format(time.RFC3339)
formatted := s.PaymentDate.UTC().Format(time.RFC3339)
details.PaymentAt = &formatted
}

View File

@@ -22,20 +22,21 @@ type PurchaseChannelInput struct {
}
type SelectChannelsForPurchase struct {
ProjectID string
ProjectTitle string
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
CurrentPage int
InvalidUsernames []string
Duplicates []string // дубликаты каналов
ParsingErrors []ParseError // ошибки парсинга с предложениями
OptionalDetailsState *PurchaseOptionalDetails // сохранённое состояние деталей
BackState bot.State
ProjectID string
ProjectTitle string
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
ProjectDefaultLinkType string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
CurrentPage int
InvalidUsernames []string
Duplicates []string // дубликаты каналов
ParsingErrors []ParseError // ошибки парсинга с предложениями
OptionalDetailsState *PurchaseOptionalDetails // сохранённое состояние деталей
BackState bot.State
}
// ParseError представляет ошибку парсинга с предложением исправления
@@ -225,12 +226,13 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
optionalDetails.Channels = s.Channels
} else {
optionalDetails = &PurchaseOptionalDetails{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
BackState: s,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectDefaultLinkType: s.ProjectDefaultLinkType,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
BackState: s,
}
}
b.SetState(optionalDetails, bot.EditMessage)

View File

@@ -130,7 +130,8 @@ func (s *DateTimePicker) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.view = pickerViewCalendar
}
case strings.HasPrefix(payload, "day:"):
s.handleDaySelect(payload)
s.handleDaySelect(payload, b)
return // confirmSelection уже делает Enter
case strings.HasPrefix(payload, "hour:"):
s.handleHourSelect(payload)
case strings.HasPrefix(payload, "min:"):
@@ -140,6 +141,7 @@ func (s *DateTimePicker) HandleCallback(b *bot.Bot, u *echotron.Update) {
return
}
// Если не было подтверждения (для дат без времени), перерисовываем
s.Enter(b, bot.EditMessage)
}
@@ -365,7 +367,7 @@ func (s *DateTimePicker) displayDate() (time.Time, bool) {
return s.selectedDate.In(MskLocation), true
}
func (s *DateTimePicker) handleDaySelect(payload string) {
func (s *DateTimePicker) handleDaySelect(payload string, b *bot.Bot) {
datePart := strings.TrimPrefix(payload, "day:")
parsed, err := time.ParseInLocation("2006-01-02", datePart, MskLocation)
if err != nil {
@@ -377,7 +379,8 @@ func (s *DateTimePicker) handleDaySelect(payload string) {
s.selectedHour = nil
s.selectedMinute = nil
} else {
s.view = pickerViewConfirm
// Сразу подтверждаем выбор для даты без времени
s.confirmSelection(b)
}
}