правки

This commit is contained in:
Artem Tsyrulnikov
2026-02-01 18:13:46 +03:00
parent 4e35c73a66
commit 52cd2ec1a0
8 changed files with 503 additions and 115 deletions

View File

@@ -15,48 +15,52 @@ import (
)
type PurchaseOptionalDetails struct {
ProjectID string
ProjectTitle 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
InputMode string
CurrentParam string
CurrentChannel string
ParamPage int
ReturnMode string
PlacementMode string
PaymentDateMode string
CostMode string
CostBeforeMode string
PurchaseTypeMode string
CommentMode string
FormatMode 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
PlacementCopy *time.Time
PaymentDateCopy *time.Time
CostCopy *CostEntry
CostBeforeCopy *CostEntry
PurchaseTypeCopy *string
CommentCopy *string
FormatCopy *string
ChannelEditMode string // "edit" или "copy"
BackState bot.State
ProjectID string
ProjectTitle 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 {
@@ -104,6 +108,7 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
))
rows = append(rows, Row(
s.formatButton(),
s.inviteLinkTypeButton(),
))
if row := s.costBeforeDeleteRow(); row != nil {
@@ -182,6 +187,16 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
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 {
@@ -332,6 +347,19 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
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)
}
}
@@ -433,6 +461,9 @@ func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
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()))
}
@@ -449,8 +480,7 @@ func (s *PurchaseOptionalDetails) formatDateTime(value *time.Time) string {
return "—"
}
local := value.In(ui2.MskLocation)
text := fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
return fmt.Sprintf("<b>%s</b>", text)
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 {
@@ -458,23 +488,21 @@ func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
return "—"
}
local := value.In(ui2.MskLocation)
text := fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
return fmt.Sprintf("<b>%s</b>", text)
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 fmt.Sprintf("<b>%s</b>", value)
return value
}
func (s *PurchaseOptionalDetails) formatCostValue() string {
if s.CostValue == nil {
return "—"
}
text := fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
}
func (s *PurchaseOptionalDetails) formatCostBefore() string {
@@ -482,8 +510,7 @@ func (s *PurchaseOptionalDetails) formatCostBefore() string {
return "—"
}
label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain)
text := fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
}
func (s *PurchaseOptionalDetails) costTypeLabel() string {
@@ -576,6 +603,22 @@ func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton
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")
}
@@ -686,6 +729,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
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>"
default:
@@ -761,6 +823,15 @@ func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMo
}
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)
@@ -789,6 +860,9 @@ func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.Render
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
}
@@ -928,7 +1002,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
// Паддинг вычисляем так, чтобы все значения начинались с одной позиции
const (
marker = "· " // Маркер пункта
valueIndent = 2 // Отступ после самого длинного названия
valueIndent = 6 // Отступ после самого длинного названия (увеличен для лучшей читаемости)
)
var result []string
@@ -936,7 +1010,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
labelLen := len([]rune(line.label))
// Паддинг = (макс.длина - тек.длина) + отступ после названия
paddingLen := maxLabelLen - labelLen + valueIndent
padding := strings.Repeat(" ", paddingLen)
padding := strings.Repeat("\u00A0", paddingLen)
formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value)
result = append(result, formattedLine)
@@ -953,6 +1027,9 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
finalResult := strings.Join(result, "\n")
// Оборачиваем в <pre> для моноширинного шрифта (выравнивание работает только в monospace)
finalResult = fmt.Sprintf("<pre>%s</pre>", finalResult)
// Логируем итоговый результат
log.Info().
Str("param", param).
@@ -1100,6 +1177,11 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
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)
@@ -1151,6 +1233,11 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
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)
@@ -1204,6 +1291,13 @@ func (s *PurchaseOptionalDetails) copyParamValue(param, channelKey string) {
} else {
s.FormatCopy = nil
}
case "invite_link_type":
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok {
copied := value
s.InviteLinkTypeCopy = &copied
} else {
s.InviteLinkTypeCopy = nil
}
}
}
@@ -1275,6 +1369,14 @@ func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
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
}
}
}
@@ -1334,6 +1436,12 @@ func (s *PurchaseOptionalDetails) pasteParamValue(param, channelKey string) {
return
}
s.FormatByChannel[channelKey] = *s.FormatCopy
case "invite_link_type":
if s.InviteLinkTypeCopy == nil {
delete(s.InviteLinkTypeByChannel, channelKey)
return
}
s.InviteLinkTypeByChannel[channelKey] = *s.InviteLinkTypeCopy
}
}
@@ -1367,6 +1475,10 @@ func (s *PurchaseOptionalDetails) applyParamToAll(param string) {
for _, ch := range s.Channels {
s.pasteParamValue(param, channelKey(ch))
}
case "invite_link_type":
for _, ch := range s.Channels {
s.pasteParamValue(param, channelKey(ch))
}
}
}
@@ -1386,6 +1498,8 @@ func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
delete(s.CommentByChannel, channelKey)
case "format":
delete(s.FormatByChannel, channelKey)
case "invite_link_type":
delete(s.InviteLinkTypeByChannel, channelKey)
}
}
@@ -1405,6 +1519,8 @@ func (s *PurchaseOptionalDetails) paramTitle(param string) string {
return "Комментарий"
case "format":
return "Формат"
case "invite_link_type":
return "Тип ссылки"
default:
return ""
}
@@ -1447,6 +1563,11 @@ func (s *PurchaseOptionalDetails) paramMode(param string) string {
return "common"
}
return s.FormatMode
case "invite_link_type":
if s.InviteLinkTypeMode == "" {
return "common"
}
return s.InviteLinkTypeMode
default:
return "common"
}
@@ -1468,6 +1589,8 @@ func (s *PurchaseOptionalDetails) setParamMode(param, mode string) {
s.CommentMode = mode
case "format":
s.FormatMode = mode
case "invite_link_type":
s.InviteLinkTypeMode = mode
}
}
@@ -1493,6 +1616,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatMode == "" {
s.FormatMode = "common"
}
if s.InviteLinkTypeMode == "" {
s.InviteLinkTypeMode = "common"
}
if s.PlacementByChannel == nil {
s.PlacementByChannel = make(map[string]*time.Time)
}
@@ -1514,6 +1640,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatByChannel == nil {
s.FormatByChannel = make(map[string]string)
}
if s.InviteLinkTypeByChannel == nil {
s.InviteLinkTypeByChannel = make(map[string]string)
}
s.syncChannelMaps()
}
@@ -1560,6 +1689,11 @@ func (s *PurchaseOptionalDetails) syncChannelMaps() {
delete(s.FormatByChannel, key)
}
}
for key := range s.InviteLinkTypeByChannel {
if _, ok := valid[key]; !ok {
delete(s.InviteLinkTypeByChannel, key)
}
}
}
func (s *PurchaseOptionalDetails) formatPlacementSummary() string {
@@ -1573,7 +1707,7 @@ func (s *PurchaseOptionalDetails) formatPlacementDetails() string {
if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("placement")
return s.renderParamChannelSummary("placement")
}
func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string {
@@ -1587,7 +1721,7 @@ func (s *PurchaseOptionalDetails) formatPaymentDateDetails() string {
if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("payment_date")
return s.renderParamChannelSummary("payment_date")
}
func (s *PurchaseOptionalDetails) formatCostSummary() string {
@@ -1601,7 +1735,7 @@ func (s *PurchaseOptionalDetails) formatCostDetails() string {
if s.CostMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("cost")
return s.renderParamChannelSummary("cost")
}
func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string {
@@ -1615,7 +1749,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeDetails() string {
if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("cost_before")
return s.renderParamChannelSummary("cost_before")
}
func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string {
@@ -1629,7 +1763,7 @@ func (s *PurchaseOptionalDetails) formatPurchaseTypeDetails() string {
if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("purchase_type")
return s.renderParamChannelSummary("purchase_type")
}
func (s *PurchaseOptionalDetails) formatCommentSummary() string {
@@ -1643,7 +1777,7 @@ func (s *PurchaseOptionalDetails) formatCommentDetails() string {
if s.CommentMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("comment")
return s.renderParamChannelSummary("comment")
}
func (s *PurchaseOptionalDetails) formatFormatSummary() string {
@@ -1657,7 +1791,21 @@ func (s *PurchaseOptionalDetails) formatFormatDetails() string {
if s.FormatMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("format")
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 {
@@ -1674,17 +1822,22 @@ func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) str
return s.formatCostBeforeForChannel(channelKey)
case "purchase_type":
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value)
return value
}
return "—"
case "comment":
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value)
return value
}
return "—"
case "format":
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value)
return value
}
return "—"
case "invite_link_type":
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
return s.inviteLinkTypeLabel(value)
}
return "—"
default:
@@ -1698,8 +1851,7 @@ func (s *PurchaseOptionalDetails) formatCostForChannel(channelKey string) string
return "—"
}
label := s.costTypeLabelForEntry(entry)
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
}
func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string {
@@ -1708,8 +1860,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string)
return "—"
}
label := s.costBeforeTypeLabelForEntry(entry)
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
}
func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
@@ -1728,6 +1879,8 @@ func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
return s.CommentCopy != nil
case "format":
return s.FormatCopy != nil
case "invite_link_type":
return s.InviteLinkTypeCopy != nil
default:
return false
}
@@ -1756,6 +1909,9 @@ func (s *PurchaseOptionalDetails) hasChannelValue(param, channelKey string) bool
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
}
@@ -1845,6 +2001,28 @@ func (s *PurchaseOptionalDetails) hasCommentValue() bool {
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 "СРМ"
@@ -1966,6 +2144,14 @@ func (s *PurchaseOptionalDetails) setFormatValue(value string) {
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"
@@ -2081,6 +2267,11 @@ func (s *PurchaseOptionalDetails) buildPlacementDetails(placementType *string) *
details.Format = &s.Format
}
}
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
if s.InviteLinkType != "" {
details.InviteLinkType = &s.InviteLinkType
}
}
if placementType != nil && (s.PurchaseTypeMode == "per_channel" || len(s.Channels) <= 1) {
details.PlacementType = placementType
}
@@ -2130,6 +2321,11 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string) *backen
details.Format = &value
}
}
if s.InviteLinkTypeMode == "per_channel" {
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
details.InviteLinkType = &value
}
}
return details
}