2376 lines
65 KiB
Go
2376 lines
65 KiB
Go
package screens
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/NicoNex/echotron/v3"
|
||
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
|
||
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||
ui2 "github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
||
"github.com/rs/zerolog/log"
|
||
)
|
||
|
||
type PurchaseOptionalDetails struct {
|
||
ProjectID string
|
||
ProjectTitle string
|
||
ProjectDefaultLinkType string
|
||
CreativeID string
|
||
CreativeTitle string
|
||
Channels []PurchaseChannelInput
|
||
PlacementDateTime *time.Time
|
||
PaymentDate *time.Time
|
||
CostType string
|
||
CostValue *float64
|
||
CostBeforeType string
|
||
CostBeforeBargain *CostEntry
|
||
PurchaseType string
|
||
Format string
|
||
Comment string
|
||
InviteLinkType string
|
||
InputMode string
|
||
CurrentParam string
|
||
CurrentChannel string
|
||
ParamPage int
|
||
ReturnMode string
|
||
PlacementMode string
|
||
PaymentDateMode string
|
||
CostMode string
|
||
CostBeforeMode string
|
||
PurchaseTypeMode string
|
||
CommentMode string
|
||
FormatMode string
|
||
InviteLinkTypeMode string
|
||
PlacementByChannel map[string]*time.Time
|
||
PaymentDateByChannel map[string]*time.Time
|
||
CostByChannel map[string]CostEntry
|
||
CostBeforeByChannel map[string]CostEntry
|
||
PurchaseTypeByChannel map[string]string
|
||
CommentByChannel map[string]string
|
||
FormatByChannel map[string]string
|
||
InviteLinkTypeByChannel map[string]string
|
||
PlacementCopy *time.Time
|
||
PaymentDateCopy *time.Time
|
||
CostCopy *CostEntry
|
||
CostBeforeCopy *CostEntry
|
||
PurchaseTypeCopy *string
|
||
CommentCopy *string
|
||
FormatCopy *string
|
||
InviteLinkTypeCopy *string
|
||
ChannelEditMode string // "edit" или "copy"
|
||
BackState bot.State
|
||
}
|
||
|
||
type CostEntry struct {
|
||
Type string
|
||
Value *float64
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||
s.ensureDefaults()
|
||
if s.InputMode != "" {
|
||
if s.InputMode == "mode_select" {
|
||
s.renderModeSelect(b, mode)
|
||
return
|
||
}
|
||
if s.renderParamScreens(b, mode) {
|
||
return
|
||
}
|
||
s.renderInputPrompt(b, mode)
|
||
return
|
||
}
|
||
|
||
text := "<b>Страница создания закупа и необязательные составляющие</b>"
|
||
|
||
text += "\n\n"
|
||
text += s.formatOptionalSummary()
|
||
|
||
var rows [][]echotron.InlineKeyboardButton
|
||
|
||
// Кнопка переключения режима (только если несколько каналов) - на первой строке
|
||
if len(s.Channels) > 1 {
|
||
rows = append(rows, Row(s.globalModeButton()))
|
||
}
|
||
|
||
rows = append(rows, Row(
|
||
s.placementButton(),
|
||
s.paymentButton(),
|
||
))
|
||
rows = append(rows, Row(
|
||
s.typeButton(),
|
||
s.commentButton(),
|
||
))
|
||
rows = append(rows, Row(
|
||
s.costButton(),
|
||
s.costBeforeButton(),
|
||
))
|
||
rows = append(rows, Row(
|
||
s.formatButton(),
|
||
s.inviteLinkTypeButton(),
|
||
))
|
||
|
||
rows = append(rows, Row(Button("Назад", "back"), Button("Далее", "next")))
|
||
|
||
keyboard := Keyboard(rows...)
|
||
|
||
b.Render(text, keyboard, mode)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||
return
|
||
}
|
||
s.ensureDefaults()
|
||
|
||
switch u.CallbackQuery.Data {
|
||
case "opt_datetime":
|
||
// Открываем редактор в текущем режиме
|
||
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "placement_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.setParamMode("placement", "common")
|
||
s.openCommonEditor(b, "placement")
|
||
}
|
||
|
||
case "opt_payment_date":
|
||
// Открываем редактор в текущем режиме
|
||
if s.PaymentDateMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "payment_date_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.setParamMode("payment_date", "common")
|
||
b.SetState(ui2.NewDateTimePicker(ui2.DateTimePickerConfig{
|
||
Title: "Дата оплаты",
|
||
Key: "payment_date",
|
||
IncludeTime: false,
|
||
AllowPast: true,
|
||
Selected: s.PaymentDate,
|
||
BackState: s,
|
||
}), bot.EditMessage)
|
||
}
|
||
|
||
case "opt_cost":
|
||
// Открываем редактор в текущем режиме
|
||
if s.CostMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "cost_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.setParamMode("cost", "common")
|
||
s.openCommonEditor(b, "cost")
|
||
}
|
||
|
||
case "opt_type":
|
||
// Открываем редактор в текущем режиме
|
||
if s.PurchaseTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "purchase_type_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.InputMode = "type"
|
||
s.Enter(b, bot.EditMessage)
|
||
}
|
||
|
||
case "opt_format":
|
||
// Открываем редактор в текущем режиме
|
||
if s.FormatMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "format_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.setParamMode("format", "common")
|
||
s.openCommonEditor(b, "format")
|
||
}
|
||
|
||
case "opt_invite_link_type":
|
||
// Открываем редактор в текущем режиме
|
||
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "invite_link_type_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.InputMode = "invite_link_type"
|
||
s.Enter(b, bot.EditMessage)
|
||
}
|
||
|
||
case "opt_comment":
|
||
// Открываем редактор в текущем режиме
|
||
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "comment_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.InputMode = "comment"
|
||
s.Enter(b, bot.EditMessage)
|
||
}
|
||
|
||
case "delete_comment":
|
||
s.Comment = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "format_custom":
|
||
s.InputMode = "format_custom"
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "cost_type_toggle":
|
||
s.toggleCostType()
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "cost_before_type_toggle":
|
||
s.toggleCostBeforeType()
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "cost_value":
|
||
s.InputMode = "cost_value"
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "cost_before":
|
||
// Открываем редактор в текущем режиме
|
||
if s.CostBeforeMode == "per_channel" && len(s.Channels) > 1 {
|
||
s.InputMode = "cost_before_channels"
|
||
s.Enter(b, bot.EditMessage)
|
||
} else {
|
||
s.setParamMode("cost_before", "common")
|
||
s.openCommonEditor(b, "cost_before")
|
||
}
|
||
|
||
case "delete_cost_before":
|
||
s.CostBeforeBargain = nil
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "back_to_optional":
|
||
s.InputMode = ""
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.CurrentParam = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
case "back_to_return":
|
||
if s.ReturnMode != "" {
|
||
s.InputMode = s.ReturnMode
|
||
s.ReturnMode = ""
|
||
} else {
|
||
s.InputMode = ""
|
||
}
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
|
||
case "back":
|
||
// Сохраняем текущее состояние перед возвратом назад
|
||
b.SetState(&SelectChannelsForPurchase{
|
||
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
|
||
if jwt == "" {
|
||
log.Error().Msg("JWT is empty in session")
|
||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
|
||
return
|
||
}
|
||
s.createPurchase(b, jwt)
|
||
case "done":
|
||
if s.BackState != nil {
|
||
b.SetState(s.BackState, bot.EditMessage)
|
||
}
|
||
default:
|
||
if u.CallbackQuery.Data == "noop" {
|
||
// Пустая кнопка - ничего не делаем
|
||
return
|
||
}
|
||
if s.handleParamCallback(b, u.CallbackQuery.Data) {
|
||
return
|
||
}
|
||
if u.CallbackQuery.Data == "toggle_global_mode" {
|
||
// Открываем меню выбора параметра для переключения режима
|
||
s.InputMode = "mode_select"
|
||
s.Enter(b, bot.EditMessage)
|
||
return
|
||
}
|
||
if strings.HasPrefix(u.CallbackQuery.Data, "channel_mode:") {
|
||
// Переключаем режим редактирования каналов
|
||
mode := strings.TrimPrefix(u.CallbackQuery.Data, "channel_mode:")
|
||
s.ChannelEditMode = mode
|
||
s.Enter(b, bot.EditMessage)
|
||
return
|
||
}
|
||
if strings.HasPrefix(u.CallbackQuery.Data, "toggle_param_mode:") {
|
||
// Переключаем режим конкретного параметра
|
||
param := strings.TrimPrefix(u.CallbackQuery.Data, "toggle_param_mode:")
|
||
currentMode := s.paramMode(param)
|
||
if currentMode == "common" {
|
||
s.setParamMode(param, "per_channel")
|
||
// Копируем значение из общего во все каналы
|
||
s.copyCommonValueToChannels(param)
|
||
} else {
|
||
s.setParamMode(param, "common")
|
||
}
|
||
// Остаемся в меню выбора режима
|
||
s.Enter(b, bot.EditMessage)
|
||
return
|
||
}
|
||
if strings.HasPrefix(u.CallbackQuery.Data, "type:") {
|
||
value := strings.TrimPrefix(u.CallbackQuery.Data, "type:")
|
||
// Нормализуем значение для БД
|
||
switch value {
|
||
case "self_promo":
|
||
s.PurchaseType = "самопиар"
|
||
case "standard":
|
||
s.PurchaseType = "стандарт"
|
||
}
|
||
s.InputMode = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
return
|
||
}
|
||
if strings.HasPrefix(u.CallbackQuery.Data, "format:") {
|
||
value := strings.TrimPrefix(u.CallbackQuery.Data, "format:")
|
||
s.setFormatValue(value)
|
||
if s.ReturnMode != "" {
|
||
s.InputMode = s.ReturnMode
|
||
s.ReturnMode = ""
|
||
} else {
|
||
s.InputMode = ""
|
||
}
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
return
|
||
}
|
||
if strings.HasPrefix(u.CallbackQuery.Data, "invite_link_type:") {
|
||
value := strings.TrimPrefix(u.CallbackQuery.Data, "invite_link_type:")
|
||
s.setInviteLinkTypeValue(value)
|
||
if s.ReturnMode != "" {
|
||
s.InputMode = s.ReturnMode
|
||
s.ReturnMode = ""
|
||
} else {
|
||
s.InputMode = ""
|
||
}
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
return
|
||
}
|
||
s.Enter(b, bot.EditMessage)
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||
if s.InputMode == "" || u.Message == nil || u.Message.Text == "" {
|
||
return
|
||
}
|
||
s.ensureDefaults()
|
||
|
||
text := strings.TrimSpace(u.Message.Text)
|
||
if text == "" {
|
||
return
|
||
}
|
||
|
||
switch s.InputMode {
|
||
case "format_custom":
|
||
s.setFormatValue(text)
|
||
case "comment":
|
||
if s.CurrentChannel != "" {
|
||
s.CommentByChannel[s.CurrentChannel] = text
|
||
} else {
|
||
s.Comment = text
|
||
}
|
||
case "cost_value":
|
||
value, err := strconv.ParseFloat(strings.ReplaceAll(text, ",", "."), 64)
|
||
if err != nil {
|
||
s.renderInputPrompt(b, bot.EditMessage)
|
||
return
|
||
}
|
||
s.setCostValue(value)
|
||
case "cost_before_value":
|
||
value, err := strconv.ParseFloat(strings.ReplaceAll(text, ",", "."), 64)
|
||
if err != nil {
|
||
s.renderInputPrompt(b, bot.EditMessage)
|
||
return
|
||
}
|
||
s.setCostBeforeValue(value)
|
||
}
|
||
|
||
if s.ReturnMode != "" {
|
||
s.InputMode = s.ReturnMode
|
||
s.ReturnMode = ""
|
||
} else {
|
||
s.InputMode = ""
|
||
}
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.NewMessage)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||
|
||
func (s *PurchaseOptionalDetails) Exit() {}
|
||
|
||
func (s *PurchaseOptionalDetails) SetDateTimeSelection(key string, value time.Time) {
|
||
switch key {
|
||
case "placement_datetime":
|
||
s.PlacementDateTime = &value
|
||
case "payment_date":
|
||
s.PaymentDate = &value
|
||
default:
|
||
if strings.HasPrefix(key, "placement_datetime:") {
|
||
username := strings.TrimPrefix(key, "placement_datetime:")
|
||
s.ensureDefaults()
|
||
s.PlacementByChannel[username] = &value
|
||
}
|
||
if strings.HasPrefix(key, "payment_date:") {
|
||
username := strings.TrimPrefix(key, "payment_date:")
|
||
s.ensureDefaults()
|
||
s.PaymentDateByChannel[username] = &value
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
|
||
var lines []string
|
||
|
||
if s.hasPlacementValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Дата размещения:</i> <b>%s</b>%s", s.formatPlacementSummary(), s.formatPlacementDetails()))
|
||
}
|
||
if s.hasPaymentDateValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Дата оплаты:</i> <b>%s</b>%s", s.formatPaymentDateSummary(), s.formatPaymentDateDetails()))
|
||
}
|
||
if s.hasCostValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Стоимость:</i> <b>%s</b>%s", s.formatCostSummary(), s.formatCostDetails()))
|
||
}
|
||
if s.hasCostBeforeValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Стоимость до торга:</i> <b>%s</b>%s", s.formatCostBeforeSummary(), s.formatCostBeforeDetails()))
|
||
}
|
||
if s.hasPurchaseTypeValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Тип закупа:</i> <b>%s</b>%s", s.formatPurchaseTypeSummary(), s.formatPurchaseTypeDetails()))
|
||
}
|
||
if s.hasFormatValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Формат:</i> <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails()))
|
||
}
|
||
if s.hasInviteLinkTypeValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Тип ссылки:</i> <b>%s</b>%s", s.formatInviteLinkTypeSummary(), s.formatInviteLinkTypeDetails()))
|
||
}
|
||
if s.hasCommentValue() {
|
||
lines = append(lines, fmt.Sprintf("<i>Комментарий:</i> <b>%s</b>%s", s.formatCommentSummary(), s.formatCommentDetails()))
|
||
}
|
||
|
||
if len(lines) == 0 {
|
||
return "<i>Добавьте параметры ниже</i>"
|
||
}
|
||
|
||
return strings.Join(lines, "\n\n")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatDateTime(value *time.Time) string {
|
||
if value == nil {
|
||
return "—"
|
||
}
|
||
local := value.In(ui2.MskLocation)
|
||
return fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
|
||
if value == nil {
|
||
return "—"
|
||
}
|
||
local := value.In(ui2.MskLocation)
|
||
return fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatText(value string) string {
|
||
if value == "" {
|
||
return "—"
|
||
}
|
||
return value
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostValue() string {
|
||
if s.CostValue == nil {
|
||
return "—"
|
||
}
|
||
return fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostBefore() string {
|
||
if s.CostBeforeBargain == nil || s.CostBeforeBargain.Value == nil {
|
||
return "—"
|
||
}
|
||
label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain)
|
||
return fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costTypeLabel() string {
|
||
if s.CostType == "cpm" {
|
||
return "СРМ"
|
||
}
|
||
return "Фикс"
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costBeforeTypeLabel() string {
|
||
costType := s.CostBeforeType
|
||
if costType == "" && s.CostBeforeBargain != nil {
|
||
costType = s.CostBeforeBargain.Type
|
||
}
|
||
if costType == "cpm" {
|
||
return "СРМ"
|
||
}
|
||
return "Фикс"
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) placementButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.hasPlacementValue() {
|
||
icon = "✎"
|
||
}
|
||
modeIcon := ""
|
||
if len(s.Channels) > 1 && s.PlacementMode == "per_channel" {
|
||
modeIcon = " 👥"
|
||
}
|
||
return Button(fmt.Sprintf("%s Дата размещения%s", icon, modeIcon), "opt_datetime")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) paymentButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.hasPaymentDateValue() {
|
||
icon = "✎"
|
||
}
|
||
modeIcon := ""
|
||
if len(s.Channels) > 1 && s.PaymentDateMode == "per_channel" {
|
||
modeIcon = " 👥"
|
||
}
|
||
return Button(fmt.Sprintf("%s Дата оплаты%s", icon, modeIcon), "opt_payment_date")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.hasCostValue() {
|
||
icon = "✎"
|
||
}
|
||
return Button(fmt.Sprintf("%s Стоимость", icon), "opt_cost")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costBeforeButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.hasCostBeforeValue() {
|
||
icon = "✎"
|
||
}
|
||
return Button(fmt.Sprintf("%s До торга", icon), "cost_before")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) typeButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.PurchaseType != "" {
|
||
icon = "✎"
|
||
}
|
||
modeIcon := ""
|
||
if len(s.Channels) > 1 && s.PurchaseTypeMode == "per_channel" {
|
||
modeIcon = " 👥"
|
||
}
|
||
return Button(fmt.Sprintf("%s Тип%s", icon, modeIcon), "opt_type")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.hasFormatValue() {
|
||
icon = "✎"
|
||
}
|
||
modeIcon := ""
|
||
if len(s.Channels) > 1 && s.FormatMode == "per_channel" {
|
||
modeIcon = " 👥"
|
||
}
|
||
return Button(fmt.Sprintf("%s Формат%s", icon, modeIcon), "opt_format")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.Comment != "" {
|
||
icon = "✎"
|
||
}
|
||
modeIcon := ""
|
||
if len(s.Channels) > 1 && s.CommentMode == "per_channel" {
|
||
modeIcon = " 👥"
|
||
}
|
||
return Button(fmt.Sprintf("%s Комментарий%s", icon, modeIcon), "opt_comment")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) inviteLinkTypeButton() echotron.InlineKeyboardButton {
|
||
icon := "+"
|
||
if s.hasInviteLinkTypeValue() {
|
||
icon = "✎"
|
||
}
|
||
modeIcon := ""
|
||
if len(s.Channels) > 1 && s.InviteLinkTypeMode == "per_channel" {
|
||
modeIcon = " 👥"
|
||
}
|
||
label := "Тип ссылки"
|
||
if s.InviteLinkType != "" {
|
||
label += ": " + s.inviteLinkTypeLabel(s.InviteLinkType)
|
||
}
|
||
return Button(fmt.Sprintf("%s %s%s", icon, label, modeIcon), "opt_invite_link_type")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton {
|
||
return Button("⚙️ Режимы параметров", "toggle_global_mode")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) commentDeleteRow() []echotron.InlineKeyboardButton {
|
||
if s.Comment == "" {
|
||
return nil
|
||
}
|
||
return Row(Button("⌫ Удалить комментарий", "delete_comment"))
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboardButton {
|
||
if s.CostBeforeBargain == nil || s.CostBeforeMode == "per_channel" {
|
||
return nil
|
||
}
|
||
return Row(Button("⌫ Удалить до торга", "delete_cost_before"))
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderMode) {
|
||
text := "<b>Дополнительно</b>\n\n"
|
||
switch s.InputMode {
|
||
case "type":
|
||
text += "<b>Выберите тип закупа</b>"
|
||
keyboard := Keyboard(
|
||
Row(Button("Самопиар", "type:self_promo"), Button("Стандарт", "type:standard")),
|
||
Row(Button("← Назад", "back_to_optional")),
|
||
)
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
return
|
||
case "cost_value":
|
||
text += "<b>Ввод стоимости</b>\n\nНапример: <code>15000</code>"
|
||
if s.CurrentChannel != "" {
|
||
text += fmt.Sprintf("\n\nКанал: <b>%s</b>", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||
}
|
||
|
||
typeLabel := s.costTypeLabelForCurrent()
|
||
keyboard := Keyboard(
|
||
Row(Button(fmt.Sprintf("Тип: %s", typeLabel), "cost_type_toggle")),
|
||
Row(Button("← Назад", s.backAction())),
|
||
)
|
||
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
return
|
||
case "cost_before_value":
|
||
text += "<b>Ввод стоимости до торга</b>\n\nНапример: <code>20000</code>"
|
||
if s.CurrentChannel != "" {
|
||
text += fmt.Sprintf("\n\nКанал: <b>%s</b>", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||
}
|
||
|
||
typeLabel := s.costBeforeTypeLabelForCurrent()
|
||
var keyboard echotron.InlineKeyboardMarkup
|
||
|
||
// Показываем кнопку удаления только если есть значение и это не режим per-channel
|
||
if s.CurrentChannel == "" && s.CostBeforeBargain != nil && s.CostBeforeBargain.Value != nil {
|
||
keyboard = Keyboard(
|
||
Row(Button(fmt.Sprintf("Тип: %s", typeLabel), "cost_before_type_toggle")),
|
||
Row(Button("⌫ Удалить до торга", "delete_cost_before")),
|
||
Row(Button("← Назад", s.backAction())),
|
||
)
|
||
} else {
|
||
keyboard = Keyboard(
|
||
Row(Button(fmt.Sprintf("Тип: %s", typeLabel), "cost_before_type_toggle")),
|
||
Row(Button("← Назад", s.backAction())),
|
||
)
|
||
}
|
||
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
return
|
||
case "format_select":
|
||
text = "<i>Страница ввода формата размещения</i>\n\n"
|
||
if s.CurrentChannel != "" {
|
||
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||
}
|
||
text += "Выберите формат\n\n"
|
||
keyboard := Keyboard(
|
||
Row(
|
||
Button("1 / 24", "format:1 / 24"),
|
||
Button("1/48", "format:1/48"),
|
||
Button("1 / 72", "format:1 / 72"),
|
||
),
|
||
Row(
|
||
Button("1 / (7 дней)", "format:1 / (7 дней)"),
|
||
Button("1 / (30 дней)", "format:1 / (30 дней)"),
|
||
Button("1 / (без удаления)", "format:1 / (без удаления)"),
|
||
),
|
||
Row(
|
||
Button("2 / 24", "format:2 / 24"),
|
||
Button("2/48", "format:2/48"),
|
||
Button("2/72", "format:2/72"),
|
||
),
|
||
Row(
|
||
Button("Назад", s.backAction()),
|
||
Button("Свой формат", "format_custom"),
|
||
),
|
||
)
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
return
|
||
case "format_custom":
|
||
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
|
||
case "invite_link_type":
|
||
text = "<i>Страница выбора типа ссылки</i>\n\n"
|
||
if s.CurrentChannel != "" {
|
||
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||
}
|
||
text += "Выберите тип ссылки\n\n"
|
||
keyboard := Keyboard(
|
||
Row(
|
||
Button("Открытая", "invite_link_type:public"),
|
||
Button("С заявками", "invite_link_type:approval"),
|
||
),
|
||
Row(Button("← Назад", s.backAction())),
|
||
)
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
return
|
||
case "comment":
|
||
text += "<b>Введите комментарий</b>"
|
||
keyboard := Keyboard(Row(Button("← Назад", s.backAction())))
|
||
if s.Comment != "" {
|
||
keyboard = Keyboard(
|
||
Row(Button("⌫ Удалить комментарий", "delete_comment")),
|
||
Row(Button("← Назад", s.backAction())),
|
||
)
|
||
}
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
return
|
||
default:
|
||
s.InputMode = ""
|
||
s.Enter(b, mode)
|
||
return
|
||
}
|
||
|
||
keyboard := Keyboard(
|
||
Row(Button("← Назад", s.backAction())),
|
||
)
|
||
|
||
if mode == bot.EditMessage {
|
||
b.Edit(text, keyboard)
|
||
} else {
|
||
b.SendNew(text, keyboard)
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMode) {
|
||
text := "<b>⚙ Переключить режим параметра</b>\n\n"
|
||
|
||
// Показываем текущую информацию о параметрах
|
||
text += s.formatOptionalSummary()
|
||
text += "\n\n"
|
||
|
||
text += "Выберите параметр для изменения режима:\n\n"
|
||
|
||
var rows [][]echotron.InlineKeyboardButton
|
||
|
||
// Дата размещения
|
||
placementLabel := "Дата размещения: "
|
||
if s.PlacementMode == "per_channel" {
|
||
placementLabel += "👥"
|
||
} else {
|
||
placementLabel += "общий"
|
||
}
|
||
rows = append(rows, Row(Button(placementLabel, "toggle_param_mode:placement")))
|
||
|
||
// Дата оплаты
|
||
paymentDateLabel := "Дата оплаты: "
|
||
if s.PaymentDateMode == "per_channel" {
|
||
paymentDateLabel += "👥"
|
||
} else {
|
||
paymentDateLabel += "общий"
|
||
}
|
||
rows = append(rows, Row(Button(paymentDateLabel, "toggle_param_mode:payment_date")))
|
||
|
||
// Тип закупа
|
||
purchaseTypeLabel := "Тип закупа: "
|
||
if s.PurchaseTypeMode == "per_channel" {
|
||
purchaseTypeLabel += "👥"
|
||
} else {
|
||
purchaseTypeLabel += "общий"
|
||
}
|
||
rows = append(rows, Row(Button(purchaseTypeLabel, "toggle_param_mode:purchase_type")))
|
||
|
||
// Комментарий
|
||
commentLabel := "Комментарий: "
|
||
if s.CommentMode == "per_channel" {
|
||
commentLabel += "👥"
|
||
} else {
|
||
commentLabel += "общий"
|
||
}
|
||
rows = append(rows, Row(Button(commentLabel, "toggle_param_mode:comment")))
|
||
|
||
// Формат
|
||
formatLabel := "Формат: "
|
||
if s.FormatMode == "per_channel" {
|
||
formatLabel += "👥"
|
||
} else {
|
||
formatLabel += "общий"
|
||
}
|
||
rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format")))
|
||
|
||
// Тип ссылки
|
||
inviteLinkTypeLabel := "Тип ссылки: "
|
||
if s.InviteLinkTypeMode == "per_channel" {
|
||
inviteLinkTypeLabel += "👥"
|
||
} else {
|
||
inviteLinkTypeLabel += "общий"
|
||
}
|
||
rows = append(rows, Row(Button(inviteLinkTypeLabel, "toggle_param_mode:invite_link_type")))
|
||
|
||
rows = append(rows, Row(Button("← Назад", "back_to_optional")))
|
||
|
||
b.Render(text, Keyboard(rows...), mode)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.RenderMode) bool {
|
||
switch s.InputMode {
|
||
case "placement_channels":
|
||
s.renderParamChannels(b, mode, "placement")
|
||
return true
|
||
case "payment_date_channels":
|
||
s.renderParamChannels(b, mode, "payment_date")
|
||
return true
|
||
case "cost_channels":
|
||
s.renderParamChannels(b, mode, "cost")
|
||
return true
|
||
case "cost_before_channels":
|
||
s.renderParamChannels(b, mode, "cost_before")
|
||
return true
|
||
case "purchase_type_channels":
|
||
s.renderParamChannels(b, mode, "purchase_type")
|
||
return true
|
||
case "comment_channels":
|
||
s.renderParamChannels(b, mode, "comment")
|
||
return true
|
||
case "format_channels":
|
||
s.renderParamChannels(b, mode, "format")
|
||
return true
|
||
case "invite_link_type_channels":
|
||
s.renderParamChannels(b, mode, "invite_link_type")
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.RenderMode, param string) {
|
||
s.CurrentParam = param
|
||
text := fmt.Sprintf("<b>%s — по каналам</b>\n\n", s.paramTitle(param))
|
||
text += s.renderParamChannelSummary(param) + "\n\n"
|
||
|
||
const perPage = 5
|
||
total := len(s.Channels)
|
||
if total == 0 {
|
||
text += "\nКаналы не выбраны"
|
||
b.Render(text, Keyboard(Row(Button("← Назад", "back_to_optional"))), mode)
|
||
return
|
||
}
|
||
|
||
if s.ParamPage*perPage >= total {
|
||
s.ParamPage = 0
|
||
}
|
||
start, end := ui2.GetPageBounds(s.ParamPage, perPage, total)
|
||
|
||
// Устанавливаем режим по умолчанию
|
||
if s.ChannelEditMode == "" {
|
||
s.ChannelEditMode = "edit"
|
||
}
|
||
|
||
var rows [][]echotron.InlineKeyboardButton
|
||
|
||
// Кнопки переключения режима
|
||
editLabel := "Редактирование"
|
||
copyLabel := "Копирование"
|
||
if s.ChannelEditMode == "edit" {
|
||
editLabel = "● " + editLabel
|
||
} else {
|
||
copyLabel = "● " + copyLabel
|
||
}
|
||
rows = append(rows, Row(
|
||
Button(editLabel, "channel_mode:edit"),
|
||
Button(copyLabel, "channel_mode:copy"),
|
||
))
|
||
|
||
// Список каналов с кнопками в зависимости от режима
|
||
for i := start; i < end; i++ {
|
||
label := fmt.Sprintf("%d", i+1)
|
||
channelKey := channelKey(s.Channels[i])
|
||
|
||
if s.ChannelEditMode == "copy" {
|
||
// Режим копирования
|
||
var channelRow []echotron.InlineKeyboardButton
|
||
channelRow = append(channelRow, Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)))
|
||
channelRow = append(channelRow, Button("⧉", fmt.Sprintf("param_copy:%s:%d", param, i)))
|
||
|
||
// Кнопка вставки - только если есть данные в буфере
|
||
if s.hasCopyBuffer(param) {
|
||
channelRow = append(channelRow, Button("⇲", fmt.Sprintf("param_paste:%s:%d", param, i)))
|
||
} else {
|
||
channelRow = append(channelRow, Button(" ", "noop"))
|
||
}
|
||
|
||
// Кнопка удаления - только если есть данные у канала
|
||
if s.hasChannelValue(param, channelKey) {
|
||
channelRow = append(channelRow, Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)))
|
||
} else {
|
||
channelRow = append(channelRow, Button(" ", "noop"))
|
||
}
|
||
|
||
rows = append(rows, channelRow)
|
||
} else {
|
||
// Обычный режим редактирования
|
||
var channelRow []echotron.InlineKeyboardButton
|
||
channelRow = append(channelRow, Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)))
|
||
|
||
// Кнопка "Добавить" или "Изменить" в зависимости от наличия данных
|
||
hasValue := s.hasChannelValue(param, channelKey)
|
||
if hasValue {
|
||
channelRow = append(channelRow, Button("Изменить", fmt.Sprintf("param_edit:%s:%d", param, i)))
|
||
} else {
|
||
channelRow = append(channelRow, Button("Добавить", fmt.Sprintf("param_edit:%s:%d", param, i)))
|
||
}
|
||
|
||
// Кнопка удаления - только если есть данные у канала
|
||
if hasValue {
|
||
channelRow = append(channelRow, Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)))
|
||
} else {
|
||
channelRow = append(channelRow, Button(" ", "noop"))
|
||
}
|
||
|
||
rows = append(rows, channelRow)
|
||
}
|
||
}
|
||
|
||
pages := ui2.CalculatePages(total, perPage)
|
||
if navRow := ui2.BuildNavigationRow(ui2.PaginationConfig{
|
||
CurrentPage: s.ParamPage,
|
||
TotalPages: pages,
|
||
}); navRow != nil {
|
||
rows = append(rows, navRow)
|
||
}
|
||
|
||
rows = append(rows, Row(
|
||
Button("← Назад", fmt.Sprintf("param_back:%s", param)),
|
||
))
|
||
|
||
b.Render(text, Keyboard(rows...), mode)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string {
|
||
// Собираем строки и вычисляем максимальную длину названия канала
|
||
type channelLine struct {
|
||
label string
|
||
value string
|
||
}
|
||
var lines []channelLine
|
||
maxLabelLen := 0
|
||
|
||
for _, ch := range s.Channels {
|
||
label := channelLabel(ch)
|
||
labelLen := len([]rune(label)) // Считаем руны для Unicode
|
||
if labelLen > maxLabelLen {
|
||
maxLabelLen = labelLen
|
||
}
|
||
channelKey := channelKey(ch)
|
||
value := s.formatParamValue(param, channelKey)
|
||
lines = append(lines, channelLine{label: label, value: value})
|
||
}
|
||
|
||
// Логирование для отладки (INFO уровень)
|
||
log.Info().
|
||
Str("param", param).
|
||
Int("maxLabelLen", maxLabelLen).
|
||
Int("channelsCount", len(lines)).
|
||
Msg("🔍 renderParamChannelSummary")
|
||
|
||
// Форматируем: маркер + название + паддинг + значение
|
||
// Паддинг вычисляем так, чтобы все значения начинались с одной позиции
|
||
const (
|
||
marker = "· " // Маркер пункта
|
||
valueIndent = 6 // Отступ после самого длинного названия (увеличен для лучшей читаемости)
|
||
)
|
||
|
||
var result []string
|
||
for i, line := range lines {
|
||
labelLen := len([]rune(line.label))
|
||
// Паддинг = (макс.длина - тек.длина) + отступ после названия
|
||
paddingLen := maxLabelLen - labelLen + valueIndent
|
||
padding := strings.Repeat("\u00A0", paddingLen)
|
||
|
||
formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value)
|
||
result = append(result, formattedLine)
|
||
|
||
// Логируем каждую строку (INFO уровень)
|
||
log.Info().
|
||
Int("index", i).
|
||
Str("label", line.label).
|
||
Int("labelLen", labelLen).
|
||
Int("paddingLen", paddingLen).
|
||
Str("formattedLine", formattedLine).
|
||
Msg("📝 Channel line")
|
||
}
|
||
|
||
finalResult := strings.Join(result, "\n")
|
||
|
||
// Оборачиваем в <pre> для моноширинного шрифта (выравнивание работает только в monospace)
|
||
finalResult = fmt.Sprintf("<pre>%s</pre>", finalResult)
|
||
|
||
// Логируем итоговый результат
|
||
log.Info().
|
||
Str("param", param).
|
||
Str("result", finalResult).
|
||
Msg("✅ Final summary")
|
||
|
||
return finalResult
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) bool {
|
||
switch {
|
||
case strings.HasPrefix(data, "param_edit:"):
|
||
param, index, ok := s.parseParamIndex(data, "param_edit:")
|
||
if !ok {
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
if index >= 0 && index < len(s.Channels) {
|
||
s.CurrentParam = param
|
||
s.CurrentChannel = channelKey(s.Channels[index])
|
||
s.openChannelEditor(b, param, s.CurrentChannel)
|
||
return true
|
||
}
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
case strings.HasPrefix(data, "param_copy:"):
|
||
param, index, ok := s.parseParamIndex(data, "param_copy:")
|
||
if !ok {
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
if index >= 0 && index < len(s.Channels) {
|
||
channelKey := channelKey(s.Channels[index])
|
||
s.copyParamValue(param, channelKey)
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
case strings.HasPrefix(data, "param_paste:"):
|
||
param, index, ok := s.parseParamIndex(data, "param_paste:")
|
||
if !ok {
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
if index >= 0 && index < len(s.Channels) {
|
||
channelKey := channelKey(s.Channels[index])
|
||
s.pasteParamValue(param, channelKey)
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
case strings.HasPrefix(data, "param_clear:"):
|
||
param, index, ok := s.parseParamIndex(data, "param_clear:")
|
||
if !ok {
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
if index >= 0 && index < len(s.Channels) {
|
||
channelKey := channelKey(s.Channels[index])
|
||
s.clearParamValue(param, channelKey)
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
case data == "prev" && strings.HasSuffix(s.InputMode, "_channels"):
|
||
if s.ParamPage > 0 {
|
||
s.ParamPage--
|
||
}
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
case data == "next" && strings.HasSuffix(s.InputMode, "_channels"):
|
||
s.ParamPage++
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
case strings.HasPrefix(data, "param_back:"):
|
||
// Возвращаемся на главный экран
|
||
s.InputMode = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) parseParamIndex(data, prefix string) (string, int, bool) {
|
||
rest := strings.TrimPrefix(data, prefix)
|
||
parts := strings.Split(rest, ":")
|
||
if len(parts) != 2 {
|
||
return "", 0, false
|
||
}
|
||
param := parts[0]
|
||
index, err := strconv.Atoi(parts[1])
|
||
if err != nil {
|
||
return "", 0, false
|
||
}
|
||
return param, index, true
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
|
||
switch param {
|
||
case "placement":
|
||
s.InputMode = ""
|
||
s.ReturnMode = ""
|
||
b.SetState(ui2.NewDateTimePicker(ui2.DateTimePickerConfig{
|
||
Title: "Дата и время размещения",
|
||
Key: "placement_datetime",
|
||
IncludeTime: true,
|
||
AllowPast: true,
|
||
Selected: s.PlacementDateTime,
|
||
BackState: s,
|
||
}), bot.EditMessage)
|
||
case "payment_date":
|
||
s.InputMode = ""
|
||
s.ReturnMode = ""
|
||
b.SetState(ui2.NewDateTimePicker(ui2.DateTimePickerConfig{
|
||
Title: "Дата оплаты",
|
||
Key: "payment_date",
|
||
IncludeTime: false,
|
||
AllowPast: true,
|
||
Selected: s.PaymentDate,
|
||
BackState: s,
|
||
}), bot.EditMessage)
|
||
case "purchase_type":
|
||
s.InputMode = "type"
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
case "comment":
|
||
s.InputMode = "comment"
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
case "cost":
|
||
s.InputMode = "cost_value"
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
case "cost_before":
|
||
s.InputMode = "cost_before_value"
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
case "format":
|
||
s.InputMode = "format_select"
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
case "invite_link_type":
|
||
s.InputMode = "invite_link_type"
|
||
s.ReturnMode = ""
|
||
s.CurrentChannel = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
default:
|
||
s.InputMode = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKey string) {
|
||
switch param {
|
||
case "placement":
|
||
s.InputMode = "placement_channels"
|
||
b.SetState(ui2.NewDateTimePicker(ui2.DateTimePickerConfig{
|
||
Title: "Дата и время размещения",
|
||
Key: "placement_datetime:" + channelKey,
|
||
IncludeTime: true,
|
||
AllowPast: true,
|
||
Selected: s.PlacementByChannel[channelKey],
|
||
BackState: s,
|
||
}), bot.EditMessage)
|
||
case "payment_date":
|
||
s.InputMode = "payment_date_channels"
|
||
b.SetState(ui2.NewDateTimePicker(ui2.DateTimePickerConfig{
|
||
Title: "Дата оплаты",
|
||
Key: "payment_date:" + channelKey,
|
||
IncludeTime: false,
|
||
AllowPast: true,
|
||
Selected: s.PaymentDateByChannel[channelKey],
|
||
BackState: s,
|
||
}), bot.EditMessage)
|
||
case "purchase_type":
|
||
s.InputMode = "type"
|
||
s.ReturnMode = "purchase_type_channels"
|
||
s.CurrentChannel = channelKey
|
||
s.Enter(b, bot.EditMessage)
|
||
case "comment":
|
||
s.InputMode = "comment"
|
||
s.ReturnMode = "comment_channels"
|
||
s.CurrentChannel = channelKey
|
||
s.Enter(b, bot.EditMessage)
|
||
case "cost":
|
||
s.InputMode = "cost_value"
|
||
s.ReturnMode = "cost_channels"
|
||
s.CurrentChannel = channelKey
|
||
s.Enter(b, bot.EditMessage)
|
||
case "cost_before":
|
||
s.InputMode = "cost_before_value"
|
||
s.ReturnMode = "cost_before_channels"
|
||
s.CurrentChannel = channelKey
|
||
s.Enter(b, bot.EditMessage)
|
||
case "format":
|
||
s.InputMode = "format_select"
|
||
s.ReturnMode = "format_channels"
|
||
s.CurrentChannel = channelKey
|
||
s.Enter(b, bot.EditMessage)
|
||
case "invite_link_type":
|
||
s.InputMode = "invite_link_type"
|
||
s.ReturnMode = "invite_link_type_channels"
|
||
s.CurrentChannel = channelKey
|
||
s.Enter(b, bot.EditMessage)
|
||
default:
|
||
s.InputMode = ""
|
||
s.Enter(b, bot.EditMessage)
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) copyParamValue(param, channelKey string) {
|
||
switch param {
|
||
case "placement":
|
||
s.PlacementCopy = s.PlacementByChannel[channelKey]
|
||
case "payment_date":
|
||
s.PaymentDateCopy = s.PaymentDateByChannel[channelKey]
|
||
case "cost":
|
||
entry := s.CostByChannel[channelKey]
|
||
copied := entry
|
||
if entry.Value == nil {
|
||
copied.Value = nil
|
||
} else {
|
||
value := *entry.Value
|
||
copied.Value = &value
|
||
}
|
||
s.CostCopy = &copied
|
||
case "cost_before":
|
||
entry := s.CostBeforeByChannel[channelKey]
|
||
copied := entry
|
||
if entry.Value == nil {
|
||
copied.Value = nil
|
||
} else {
|
||
value := *entry.Value
|
||
copied.Value = &value
|
||
}
|
||
s.CostBeforeCopy = &copied
|
||
case "purchase_type":
|
||
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok {
|
||
copied := value
|
||
s.PurchaseTypeCopy = &copied
|
||
} else {
|
||
s.PurchaseTypeCopy = nil
|
||
}
|
||
case "comment":
|
||
if value, ok := s.CommentByChannel[channelKey]; ok {
|
||
copied := value
|
||
s.CommentCopy = &copied
|
||
} else {
|
||
s.CommentCopy = nil
|
||
}
|
||
case "format":
|
||
if value, ok := s.FormatByChannel[channelKey]; ok {
|
||
copied := value
|
||
s.FormatCopy = &copied
|
||
} else {
|
||
s.FormatCopy = nil
|
||
}
|
||
case "invite_link_type":
|
||
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok {
|
||
copied := value
|
||
s.InviteLinkTypeCopy = &copied
|
||
} else {
|
||
s.InviteLinkTypeCopy = nil
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
|
||
switch param {
|
||
case "placement":
|
||
if s.PlacementDateTime == nil {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
value := s.PlacementDateTime
|
||
s.PlacementByChannel[key] = value
|
||
}
|
||
case "payment_date":
|
||
if s.PaymentDate == nil {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
value := s.PaymentDate
|
||
s.PaymentDateByChannel[key] = value
|
||
}
|
||
case "cost":
|
||
if s.CostValue == nil {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
entry := CostEntry{
|
||
Type: s.CostType,
|
||
Value: s.CostValue,
|
||
}
|
||
s.CostByChannel[key] = entry
|
||
}
|
||
case "cost_before":
|
||
if s.CostBeforeBargain == nil || s.CostBeforeBargain.Value == nil {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
entry := CostEntry{
|
||
Type: s.CostBeforeBargain.Type,
|
||
Value: s.CostBeforeBargain.Value,
|
||
}
|
||
s.CostBeforeByChannel[key] = entry
|
||
}
|
||
case "purchase_type":
|
||
if s.PurchaseType == "" {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
s.PurchaseTypeByChannel[key] = s.PurchaseType
|
||
}
|
||
case "comment":
|
||
if s.Comment == "" {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
s.CommentByChannel[key] = s.Comment
|
||
}
|
||
case "format":
|
||
if s.Format == "" {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
s.FormatByChannel[key] = s.Format
|
||
}
|
||
case "invite_link_type":
|
||
if s.InviteLinkType == "" {
|
||
return
|
||
}
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
s.InviteLinkTypeByChannel[key] = s.InviteLinkType
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) pasteParamValue(param, channelKey string) {
|
||
switch param {
|
||
case "placement":
|
||
if s.PlacementCopy == nil {
|
||
s.PlacementByChannel[channelKey] = nil
|
||
return
|
||
}
|
||
value := s.PlacementCopy.In(ui2.MskLocation)
|
||
s.PlacementByChannel[channelKey] = &value
|
||
case "payment_date":
|
||
if s.PaymentDateCopy == nil {
|
||
s.PaymentDateByChannel[channelKey] = nil
|
||
return
|
||
}
|
||
value := s.PaymentDateCopy.In(ui2.MskLocation)
|
||
s.PaymentDateByChannel[channelKey] = &value
|
||
case "cost":
|
||
if s.CostCopy == nil {
|
||
delete(s.CostByChannel, channelKey)
|
||
return
|
||
}
|
||
copied := *s.CostCopy
|
||
if copied.Value != nil {
|
||
value := *copied.Value
|
||
copied.Value = &value
|
||
}
|
||
s.CostByChannel[channelKey] = copied
|
||
case "cost_before":
|
||
if s.CostBeforeCopy == nil {
|
||
delete(s.CostBeforeByChannel, channelKey)
|
||
return
|
||
}
|
||
copied := *s.CostBeforeCopy
|
||
if copied.Value != nil {
|
||
value := *copied.Value
|
||
copied.Value = &value
|
||
}
|
||
s.CostBeforeByChannel[channelKey] = copied
|
||
case "purchase_type":
|
||
if s.PurchaseTypeCopy == nil {
|
||
delete(s.PurchaseTypeByChannel, channelKey)
|
||
return
|
||
}
|
||
s.PurchaseTypeByChannel[channelKey] = *s.PurchaseTypeCopy
|
||
case "comment":
|
||
if s.CommentCopy == nil {
|
||
delete(s.CommentByChannel, channelKey)
|
||
return
|
||
}
|
||
s.CommentByChannel[channelKey] = *s.CommentCopy
|
||
case "format":
|
||
if s.FormatCopy == nil {
|
||
delete(s.FormatByChannel, channelKey)
|
||
return
|
||
}
|
||
s.FormatByChannel[channelKey] = *s.FormatCopy
|
||
case "invite_link_type":
|
||
if s.InviteLinkTypeCopy == nil {
|
||
delete(s.InviteLinkTypeByChannel, channelKey)
|
||
return
|
||
}
|
||
s.InviteLinkTypeByChannel[channelKey] = *s.InviteLinkTypeCopy
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) applyParamToAll(param string) {
|
||
switch param {
|
||
case "placement":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "payment_date":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "cost":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "cost_before":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "purchase_type":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "comment":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "format":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
case "invite_link_type":
|
||
for _, ch := range s.Channels {
|
||
s.pasteParamValue(param, channelKey(ch))
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
|
||
switch param {
|
||
case "placement":
|
||
s.PlacementByChannel[channelKey] = nil
|
||
case "payment_date":
|
||
s.PaymentDateByChannel[channelKey] = nil
|
||
case "cost":
|
||
delete(s.CostByChannel, channelKey)
|
||
case "cost_before":
|
||
delete(s.CostBeforeByChannel, channelKey)
|
||
case "purchase_type":
|
||
delete(s.PurchaseTypeByChannel, channelKey)
|
||
case "comment":
|
||
delete(s.CommentByChannel, channelKey)
|
||
case "format":
|
||
delete(s.FormatByChannel, channelKey)
|
||
case "invite_link_type":
|
||
delete(s.InviteLinkTypeByChannel, channelKey)
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) paramTitle(param string) string {
|
||
switch param {
|
||
case "placement":
|
||
return "Дата и время размещения"
|
||
case "payment_date":
|
||
return "Дата оплаты"
|
||
case "cost":
|
||
return "Стоимость"
|
||
case "cost_before":
|
||
return "Стоимость до торга"
|
||
case "purchase_type":
|
||
return "Тип закупа"
|
||
case "comment":
|
||
return "Комментарий"
|
||
case "format":
|
||
return "Формат"
|
||
case "invite_link_type":
|
||
return "Тип ссылки"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) paramMode(param string) string {
|
||
switch param {
|
||
case "placement":
|
||
if s.PlacementMode == "" {
|
||
return "common"
|
||
}
|
||
return s.PlacementMode
|
||
case "payment_date":
|
||
if s.PaymentDateMode == "" {
|
||
return "common"
|
||
}
|
||
return s.PaymentDateMode
|
||
case "cost":
|
||
if s.CostMode == "" {
|
||
return "per_channel"
|
||
}
|
||
return s.CostMode
|
||
case "cost_before":
|
||
if s.CostBeforeMode == "" {
|
||
return "per_channel"
|
||
}
|
||
return s.CostBeforeMode
|
||
case "purchase_type":
|
||
if s.PurchaseTypeMode == "" {
|
||
return "common"
|
||
}
|
||
return s.PurchaseTypeMode
|
||
case "comment":
|
||
if s.CommentMode == "" {
|
||
return "common"
|
||
}
|
||
return s.CommentMode
|
||
case "format":
|
||
if s.FormatMode == "" {
|
||
return "common"
|
||
}
|
||
return s.FormatMode
|
||
case "invite_link_type":
|
||
if s.InviteLinkTypeMode == "" {
|
||
return "common"
|
||
}
|
||
return s.InviteLinkTypeMode
|
||
default:
|
||
return "common"
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) setParamMode(param, mode string) {
|
||
switch param {
|
||
case "placement":
|
||
s.PlacementMode = mode
|
||
case "payment_date":
|
||
s.PaymentDateMode = mode
|
||
case "cost":
|
||
s.CostMode = mode
|
||
case "cost_before":
|
||
s.CostBeforeMode = mode
|
||
case "purchase_type":
|
||
s.PurchaseTypeMode = mode
|
||
case "comment":
|
||
s.CommentMode = mode
|
||
case "format":
|
||
s.FormatMode = mode
|
||
case "invite_link_type":
|
||
s.InviteLinkTypeMode = mode
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) ensureDefaults() {
|
||
if s.PlacementMode == "" {
|
||
s.PlacementMode = "common"
|
||
}
|
||
if s.PaymentDateMode == "" {
|
||
s.PaymentDateMode = "common"
|
||
}
|
||
if s.CostMode == "" {
|
||
s.CostMode = "per_channel"
|
||
}
|
||
if s.CostBeforeMode == "" {
|
||
s.CostBeforeMode = "per_channel"
|
||
}
|
||
if s.PurchaseTypeMode == "" {
|
||
s.PurchaseTypeMode = "common"
|
||
}
|
||
if s.CommentMode == "" {
|
||
s.CommentMode = "common"
|
||
}
|
||
if s.FormatMode == "" {
|
||
s.FormatMode = "common"
|
||
}
|
||
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)
|
||
}
|
||
if s.PaymentDateByChannel == nil {
|
||
s.PaymentDateByChannel = make(map[string]*time.Time)
|
||
}
|
||
if s.CostByChannel == nil {
|
||
s.CostByChannel = make(map[string]CostEntry)
|
||
}
|
||
if s.CostBeforeByChannel == nil {
|
||
s.CostBeforeByChannel = make(map[string]CostEntry)
|
||
}
|
||
if s.PurchaseTypeByChannel == nil {
|
||
s.PurchaseTypeByChannel = make(map[string]string)
|
||
}
|
||
if s.CommentByChannel == nil {
|
||
s.CommentByChannel = make(map[string]string)
|
||
}
|
||
if s.FormatByChannel == nil {
|
||
s.FormatByChannel = make(map[string]string)
|
||
}
|
||
if s.InviteLinkTypeByChannel == nil {
|
||
s.InviteLinkTypeByChannel = make(map[string]string)
|
||
}
|
||
s.syncChannelMaps()
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) syncChannelMaps() {
|
||
valid := make(map[string]struct{}, len(s.Channels))
|
||
for _, ch := range s.Channels {
|
||
key := channelKey(ch)
|
||
if key != "" {
|
||
valid[key] = struct{}{}
|
||
}
|
||
}
|
||
for key := range s.PlacementByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.PlacementByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.PaymentDateByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.PaymentDateByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.CostByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.CostByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.CostBeforeByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.CostBeforeByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.PurchaseTypeByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.PurchaseTypeByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.CommentByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.CommentByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.FormatByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.FormatByChannel, key)
|
||
}
|
||
}
|
||
for key := range s.InviteLinkTypeByChannel {
|
||
if _, ok := valid[key]; !ok {
|
||
delete(s.InviteLinkTypeByChannel, key)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatPlacementSummary() string {
|
||
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatDateTime(s.PlacementDateTime)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatPlacementDetails() string {
|
||
if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("placement")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string {
|
||
if s.PaymentDateMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatDate(s.PaymentDate)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatPaymentDateDetails() string {
|
||
if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("payment_date")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostSummary() string {
|
||
if s.CostMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatCostValue()
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostDetails() string {
|
||
if s.CostMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("cost")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string {
|
||
if s.CostBeforeMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatCostBefore()
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostBeforeDetails() string {
|
||
if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("cost_before")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string {
|
||
if s.PurchaseTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatText(s.PurchaseType)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatPurchaseTypeDetails() string {
|
||
if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("purchase_type")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCommentSummary() string {
|
||
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatText(s.Comment)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCommentDetails() string {
|
||
if s.CommentMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("comment")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatFormatSummary() string {
|
||
if s.FormatMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.formatText(s.Format)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatFormatDetails() string {
|
||
if s.FormatMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("format")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatInviteLinkTypeSummary() string {
|
||
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
return "👥"
|
||
}
|
||
return s.inviteLinkTypeLabel(s.InviteLinkType)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatInviteLinkTypeDetails() string {
|
||
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
|
||
return ""
|
||
}
|
||
return s.renderParamChannelSummary("invite_link_type")
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) string {
|
||
switch param {
|
||
case "placement":
|
||
value := s.PlacementByChannel[channelKey]
|
||
return s.formatDateTime(value)
|
||
case "payment_date":
|
||
value := s.PaymentDateByChannel[channelKey]
|
||
return s.formatDate(value)
|
||
case "cost":
|
||
return s.formatCostForChannel(channelKey)
|
||
case "cost_before":
|
||
return s.formatCostBeforeForChannel(channelKey)
|
||
case "purchase_type":
|
||
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
|
||
return value
|
||
}
|
||
return "—"
|
||
case "comment":
|
||
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
|
||
return value
|
||
}
|
||
return "—"
|
||
case "format":
|
||
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
|
||
return value
|
||
}
|
||
return "—"
|
||
case "invite_link_type":
|
||
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
|
||
return s.inviteLinkTypeLabel(value)
|
||
}
|
||
return "—"
|
||
default:
|
||
return "—"
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostForChannel(channelKey string) string {
|
||
entry, ok := s.CostByChannel[channelKey]
|
||
if !ok || entry.Value == nil {
|
||
return "—"
|
||
}
|
||
label := s.costTypeLabelForEntry(entry)
|
||
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string {
|
||
entry, ok := s.CostBeforeByChannel[channelKey]
|
||
if !ok || entry.Value == nil {
|
||
return "—"
|
||
}
|
||
label := s.costBeforeTypeLabelForEntry(entry)
|
||
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
|
||
switch param {
|
||
case "placement":
|
||
return s.PlacementCopy != nil
|
||
case "payment_date":
|
||
return s.PaymentDateCopy != nil
|
||
case "cost":
|
||
return s.CostCopy != nil
|
||
case "cost_before":
|
||
return s.CostBeforeCopy != nil
|
||
case "purchase_type":
|
||
return s.PurchaseTypeCopy != nil
|
||
case "comment":
|
||
return s.CommentCopy != nil
|
||
case "format":
|
||
return s.FormatCopy != nil
|
||
case "invite_link_type":
|
||
return s.InviteLinkTypeCopy != nil
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasChannelValue(param, channelKey string) bool {
|
||
switch param {
|
||
case "placement":
|
||
value, ok := s.PlacementByChannel[channelKey]
|
||
return ok && value != nil
|
||
case "payment_date":
|
||
value, ok := s.PaymentDateByChannel[channelKey]
|
||
return ok && value != nil
|
||
case "cost":
|
||
entry, ok := s.CostByChannel[channelKey]
|
||
return ok && entry.Value != nil
|
||
case "cost_before":
|
||
entry, ok := s.CostBeforeByChannel[channelKey]
|
||
return ok && entry.Value != nil
|
||
case "purchase_type":
|
||
value, ok := s.PurchaseTypeByChannel[channelKey]
|
||
return ok && value != ""
|
||
case "comment":
|
||
value, ok := s.CommentByChannel[channelKey]
|
||
return ok && value != ""
|
||
case "format":
|
||
value, ok := s.FormatByChannel[channelKey]
|
||
return ok && value != ""
|
||
case "invite_link_type":
|
||
value, ok := s.InviteLinkTypeByChannel[channelKey]
|
||
return ok && value != ""
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasPlacementValue() bool {
|
||
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, value := range s.PlacementByChannel {
|
||
if value != nil {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.PlacementDateTime != nil
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasPaymentDateValue() bool {
|
||
if s.PaymentDateMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, value := range s.PaymentDateByChannel {
|
||
if value != nil {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.PaymentDate != nil
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasCostValue() bool {
|
||
if s.CostMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, entry := range s.CostByChannel {
|
||
if entry.Value != nil {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.CostValue != nil
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasCostBeforeValue() bool {
|
||
if s.CostBeforeMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, entry := range s.CostBeforeByChannel {
|
||
if entry.Value != nil {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.CostBeforeBargain != nil && s.CostBeforeBargain.Value != nil
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasFormatValue() bool {
|
||
if s.FormatMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, value := range s.FormatByChannel {
|
||
if value != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.Format != ""
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasPurchaseTypeValue() bool {
|
||
if s.PurchaseTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, value := range s.PurchaseTypeByChannel {
|
||
if value != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.PurchaseType != ""
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasCommentValue() bool {
|
||
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, value := range s.CommentByChannel {
|
||
if value != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.Comment != ""
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) hasInviteLinkTypeValue() bool {
|
||
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
for _, value := range s.InviteLinkTypeByChannel {
|
||
if value != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
return s.InviteLinkType != ""
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) inviteLinkTypeLabel(linkType string) string {
|
||
if linkType == "public" {
|
||
return "Открытая"
|
||
}
|
||
if linkType == "approval" {
|
||
return "С заявками"
|
||
}
|
||
return "—"
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costTypeLabelForEntry(entry CostEntry) string {
|
||
if entry.Type == "cpm" {
|
||
return "СРМ"
|
||
}
|
||
return "Фикс"
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costBeforeTypeLabelForEntry(entry CostEntry) string {
|
||
if entry.Type == "cpm" {
|
||
return "СРМ"
|
||
}
|
||
return "Фикс"
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costTypeLabelForCurrent() string {
|
||
if s.CurrentChannel == "" {
|
||
return s.costTypeLabel()
|
||
}
|
||
entry, ok := s.CostByChannel[s.CurrentChannel]
|
||
if !ok || entry.Type == "" {
|
||
return s.costTypeLabel()
|
||
}
|
||
return s.costTypeLabelForEntry(entry)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) costBeforeTypeLabelForCurrent() string {
|
||
if s.CurrentChannel == "" {
|
||
return s.costBeforeTypeLabel()
|
||
}
|
||
entry, ok := s.CostBeforeByChannel[s.CurrentChannel]
|
||
if !ok || entry.Type == "" {
|
||
return s.costBeforeTypeLabel()
|
||
}
|
||
return s.costBeforeTypeLabelForEntry(entry)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) toggleCostType() {
|
||
if s.CurrentChannel == "" {
|
||
if s.costTypeLabel() == "СРМ" {
|
||
s.CostType = "fixed"
|
||
} else {
|
||
s.CostType = "cpm"
|
||
}
|
||
return
|
||
}
|
||
entry := s.CostByChannel[s.CurrentChannel]
|
||
if entry.Type == "" {
|
||
entry.Type = s.CostType
|
||
}
|
||
if s.costTypeLabelForEntry(entry) == "СРМ" {
|
||
entry.Type = "fixed"
|
||
} else {
|
||
entry.Type = "cpm"
|
||
}
|
||
s.CostByChannel[s.CurrentChannel] = entry
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) toggleCostBeforeType() {
|
||
if s.CurrentChannel == "" {
|
||
if s.costBeforeTypeLabel() == "СРМ" {
|
||
s.CostBeforeType = "fixed"
|
||
} else {
|
||
s.CostBeforeType = "cpm"
|
||
}
|
||
if s.CostBeforeBargain != nil {
|
||
s.CostBeforeBargain.Type = s.CostBeforeType
|
||
}
|
||
return
|
||
}
|
||
entry := s.CostBeforeByChannel[s.CurrentChannel]
|
||
if entry.Type == "" {
|
||
entry.Type = s.CostBeforeType
|
||
}
|
||
if s.costBeforeTypeLabelForEntry(entry) == "СРМ" {
|
||
entry.Type = "fixed"
|
||
} else {
|
||
entry.Type = "cpm"
|
||
}
|
||
s.CostBeforeByChannel[s.CurrentChannel] = entry
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) setCostValue(value float64) {
|
||
if s.CurrentChannel == "" {
|
||
s.CostValue = &value
|
||
return
|
||
}
|
||
entry := s.CostByChannel[s.CurrentChannel]
|
||
entry.Value = &value
|
||
if entry.Type == "" {
|
||
entry.Type = s.CostType
|
||
}
|
||
s.CostByChannel[s.CurrentChannel] = entry
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) setCostBeforeValue(value float64) {
|
||
if s.CurrentChannel == "" {
|
||
if s.CostBeforeBargain == nil {
|
||
s.CostBeforeBargain = &CostEntry{}
|
||
}
|
||
s.CostBeforeBargain.Value = &value
|
||
if s.CostBeforeBargain.Type == "" {
|
||
s.CostBeforeBargain.Type = s.CostBeforeType
|
||
}
|
||
return
|
||
}
|
||
entry := s.CostBeforeByChannel[s.CurrentChannel]
|
||
entry.Value = &value
|
||
if entry.Type == "" {
|
||
entry.Type = s.CostBeforeType
|
||
}
|
||
s.CostBeforeByChannel[s.CurrentChannel] = entry
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) setFormatValue(value string) {
|
||
if s.CurrentChannel == "" {
|
||
s.Format = value
|
||
return
|
||
}
|
||
s.FormatByChannel[s.CurrentChannel] = value
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) setInviteLinkTypeValue(value string) {
|
||
if s.CurrentChannel == "" {
|
||
s.InviteLinkType = value
|
||
return
|
||
}
|
||
s.InviteLinkTypeByChannel[s.CurrentChannel] = value
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) backAction() string {
|
||
if s.ReturnMode != "" {
|
||
return "back_to_return"
|
||
}
|
||
return "back_to_optional"
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
|
||
if len(s.Channels) == 0 {
|
||
b.SendNew("❌ Добавьте хотя бы один канал", Keyboard(
|
||
Row(Button("← Назад", "back")),
|
||
))
|
||
return
|
||
}
|
||
|
||
placementType, ok := s.normalizePurchaseType()
|
||
if !ok {
|
||
b.SendNew("❌ Тип закупа: используйте «самопиар» или «стандарт»", Keyboard(
|
||
Row(Button("← Назад", "back")),
|
||
))
|
||
return
|
||
}
|
||
|
||
apiChannels := make([]backend.CreatePlacementChannelInput, 0, len(s.Channels))
|
||
for _, ch := range s.Channels {
|
||
channelDetails := s.buildChannelDetails(channelKey(ch), placementType)
|
||
if channelDetails != nil && s.isChannelDetailsEmpty(channelDetails) {
|
||
channelDetails = nil
|
||
}
|
||
apiChannels = append(apiChannels, backend.CreatePlacementChannelInput{
|
||
ChannelID: ch.ChannelID,
|
||
Comment: ch.Comment,
|
||
Details: channelDetails,
|
||
})
|
||
}
|
||
|
||
input := backend.CreatePlacementsInput{
|
||
CreativeID: &s.CreativeID,
|
||
Channels: apiChannels,
|
||
}
|
||
|
||
placements, err := b.Backend.CreatePlacements(context.Background(), jwt, b.Session.WorkspaceID, s.ProjectID, input)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to create placements")
|
||
b.SendNew("❌ Не удалось создать размещения", Keyboard(
|
||
Row(Button("← Назад", "back")),
|
||
))
|
||
return
|
||
}
|
||
|
||
for _, placement := range placements.Placements {
|
||
_, err := b.Backend.BuildPlacementCreative(
|
||
context.Background(),
|
||
jwt,
|
||
b.Session.WorkspaceID,
|
||
s.ProjectID,
|
||
placement.ID,
|
||
)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to build placement creative")
|
||
continue
|
||
}
|
||
}
|
||
|
||
// Очищаем сохранённое состояние после успешного создания
|
||
// Находим SelectChannelsForPurchase в BackState и очищаем его OptionalDetailsState
|
||
if selectState, ok := s.BackState.(*SelectChannelsForPurchase); ok {
|
||
selectState.OptionalDetailsState = nil
|
||
}
|
||
|
||
b.SetState(&Placements{
|
||
ProjectID: s.ProjectID,
|
||
ProjectTitle: s.ProjectTitle,
|
||
BackState: s.BackState,
|
||
}, bot.NewMessage)
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string, placementType *string) *backend.PlacementDetails {
|
||
details := &backend.PlacementDetails{}
|
||
|
||
// Placement date
|
||
if s.PlacementMode == "per_channel" && len(s.Channels) > 1 {
|
||
if value, ok := s.PlacementByChannel[channelKey]; ok && value != nil {
|
||
formatted := value.UTC().Format(time.RFC3339)
|
||
details.PlacementAt = &formatted
|
||
}
|
||
} else if s.PlacementDateTime != nil {
|
||
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.UTC().Format(time.RFC3339)
|
||
details.PaymentAt = &formatted
|
||
}
|
||
} else if s.PaymentDate != nil {
|
||
formatted := s.PaymentDate.UTC().Format(time.RFC3339)
|
||
details.PaymentAt = &formatted
|
||
}
|
||
|
||
// Cost
|
||
if s.CostMode == "per_channel" && len(s.Channels) > 1 {
|
||
entry := s.CostByChannel[channelKey]
|
||
details.Cost = s.buildCostInfo(entry.Type, entry.Value)
|
||
} else {
|
||
details.Cost = s.buildCostInfo(s.CostType, s.CostValue)
|
||
}
|
||
|
||
// Cost before bargain
|
||
if s.CostBeforeMode == "per_channel" && len(s.Channels) > 1 {
|
||
if entry, ok := s.CostBeforeByChannel[channelKey]; ok && entry.Value != nil {
|
||
details.CostBeforeBargain = s.buildCostInfo(entry.Type, entry.Value)
|
||
}
|
||
} else if s.CostBeforeBargain != nil && s.CostBeforeBargain.Value != nil {
|
||
details.CostBeforeBargain = s.buildCostInfo(s.CostBeforeBargain.Type, s.CostBeforeBargain.Value)
|
||
}
|
||
|
||
// Placement type
|
||
if s.PurchaseTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
|
||
details.PlacementType = &value
|
||
}
|
||
} else if placementType != nil {
|
||
details.PlacementType = placementType
|
||
}
|
||
|
||
// Comment
|
||
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
|
||
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
|
||
details.Comment = &value
|
||
}
|
||
} else if s.Comment != "" {
|
||
details.Comment = &s.Comment
|
||
}
|
||
|
||
// Format
|
||
if s.FormatMode == "per_channel" && len(s.Channels) > 1 {
|
||
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
|
||
details.Format = &value
|
||
}
|
||
} else if s.Format != "" {
|
||
details.Format = &s.Format
|
||
}
|
||
|
||
// Invite link type
|
||
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
|
||
details.InviteLinkType = &value
|
||
}
|
||
} else if s.InviteLinkType != "" {
|
||
details.InviteLinkType = &s.InviteLinkType
|
||
}
|
||
|
||
return details
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) buildCostInfo(costType string, value *float64) *backend.CostInfo {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
normalized := "fixed"
|
||
if costType == "cpm" {
|
||
normalized = "cpm"
|
||
}
|
||
return &backend.CostInfo{
|
||
Type: normalized,
|
||
Value: *value,
|
||
}
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) isChannelDetailsEmpty(details *backend.PlacementDetails) bool {
|
||
return details.PlacementAt == nil &&
|
||
details.PaymentAt == nil &&
|
||
details.Cost == nil &&
|
||
details.CostBeforeBargain == nil &&
|
||
details.PlacementType == nil &&
|
||
details.Format == nil &&
|
||
details.Comment == nil &&
|
||
details.InviteLinkType == nil
|
||
}
|
||
|
||
func (s *PurchaseOptionalDetails) normalizePurchaseType() (*string, bool) {
|
||
raw := strings.TrimSpace(s.PurchaseType)
|
||
if raw == "" {
|
||
return nil, true
|
||
}
|
||
normalized := strings.ToLower(raw)
|
||
normalized = strings.TrimSpace(strings.Trim(normalized, "."))
|
||
switch normalized {
|
||
case "самопиар", "само пиар", "self_promo", "self promo", "вп", "vp":
|
||
value := "self_promo"
|
||
return &value, true
|
||
case "стандарт", "standard":
|
||
value := "standard"
|
||
return &value, true
|
||
default:
|
||
return nil, false
|
||
}
|
||
}
|