fix: ввод даты, ввод формата

This commit is contained in:
Artem Tsyrulnikov
2026-02-28 13:06:30 +03:00
parent 0ad9fe36a6
commit c9d5f1e2ea
14 changed files with 1012 additions and 212 deletions

View File

@@ -29,6 +29,13 @@ type PurchaseOptionalDetails struct {
CostBeforeBargain *CostEntry
PurchaseType string
Format string
TopTimeMinutes *int
FeedTimeMinutes *int // nil = не указан, 0 = без удаления
TopTimeByChannel map[string]*int
FeedTimeByChannel map[string]*int
CustomFormatTopUnit string // "hours" | "minutes"
CustomFormatFeedUnit string // "hours" | "days"
CustomFormatTopValue *int // промежуточное значение (в минутах)
Comment string
InviteLinkType string
InputMode string
@@ -209,6 +216,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
case "format_custom":
s.InputMode = "format_custom"
s.CustomFormatTopUnit = "hours"
s.CustomFormatFeedUnit = "hours"
s.CustomFormatTopValue = nil
s.Enter(b, bot.EditMessage)
case "cost_type_toggle":
@@ -328,6 +338,90 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage)
return
}
if strings.HasPrefix(u.CallbackQuery.Data, "format_preset:") {
parts := strings.Split(strings.TrimPrefix(u.CallbackQuery.Data, "format_preset:"), ":")
if len(parts) == 2 {
topMin, err1 := strconv.Atoi(parts[0])
feedMin, err2 := strconv.Atoi(parts[1])
if err1 == nil && err2 == nil {
s.setFormatPreset(topMin, feedMin)
}
}
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, "format_custom_top:") {
valStr := strings.TrimPrefix(u.CallbackQuery.Data, "format_custom_top:")
minutes, err := strconv.Atoi(valStr)
if err == nil && minutes > 0 {
s.CustomFormatTopValue = &minutes
s.InputMode = "format_custom_feed"
}
s.Enter(b, bot.EditMessage)
return
}
if strings.HasPrefix(u.CallbackQuery.Data, "format_custom_feed:") {
valStr := strings.TrimPrefix(u.CallbackQuery.Data, "format_custom_feed:")
feedMinutes, err := strconv.Atoi(valStr)
if err == nil && s.CustomFormatTopValue != nil {
s.setFormatPreset(*s.CustomFormatTopValue, feedMinutes)
s.CustomFormatTopValue = nil
if s.ReturnMode != "" {
s.InputMode = s.ReturnMode
s.ReturnMode = ""
} else {
s.InputMode = ""
}
s.CurrentChannel = ""
}
s.Enter(b, bot.EditMessage)
return
}
if u.CallbackQuery.Data == "format_custom_top_toggle_unit" {
if s.CustomFormatTopUnit == "hours" {
s.CustomFormatTopUnit = "minutes"
} else {
s.CustomFormatTopUnit = "hours"
}
s.Enter(b, bot.EditMessage)
return
}
if u.CallbackQuery.Data == "format_custom_feed_toggle_unit" {
if s.CustomFormatFeedUnit == "hours" {
s.CustomFormatFeedUnit = "days"
} else {
s.CustomFormatFeedUnit = "hours"
}
s.Enter(b, bot.EditMessage)
return
}
if u.CallbackQuery.Data == "format_custom_top_input_mode" {
s.InputMode = "format_custom_top_input"
s.Enter(b, bot.EditMessage)
return
}
if u.CallbackQuery.Data == "format_custom_feed_input_mode" {
s.InputMode = "format_custom_feed_input"
s.Enter(b, bot.EditMessage)
return
}
if u.CallbackQuery.Data == "format_custom_back_to_select" {
s.InputMode = "format_select"
s.Enter(b, bot.EditMessage)
return
}
if u.CallbackQuery.Data == "format_custom_back_to_top" {
s.InputMode = "format_custom"
s.Enter(b, bot.EditMessage)
return
}
if strings.HasPrefix(u.CallbackQuery.Data, "format:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "format:")
s.setFormatValue(value)
@@ -370,8 +464,36 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
}
switch s.InputMode {
case "format_custom":
s.setFormatValue(text)
case "format_custom_top_input":
value, err := strconv.Atoi(text)
if err != nil || value <= 0 {
s.renderInputPrompt(b, bot.EditMessage)
return
}
// Конвертируем в минуты по текущей единице
if s.CustomFormatTopUnit == "hours" {
value = value * 60
}
s.CustomFormatTopValue = &value
s.InputMode = "format_custom_feed"
s.Enter(b, bot.NewMessage)
return
case "format_custom_feed_input":
value, err := strconv.Atoi(text)
if err != nil || value <= 0 {
s.renderInputPrompt(b, bot.EditMessage)
return
}
// Конвертируем в минуты по текущей единице
if s.CustomFormatFeedUnit == "days" {
value = value * 24 * 60
} else {
value = value * 60
}
if s.CustomFormatTopValue != nil {
s.setFormatPreset(*s.CustomFormatTopValue, value)
s.CustomFormatTopValue = nil
}
case "comment":
if s.CurrentChannel != "" {
s.CommentByChannel[s.CurrentChannel] = text
@@ -699,22 +821,34 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
text += "Выберите формат\n\n"
keyboard := Keyboard(
Row(
Button("1 / 24", "format:1 / 24"),
Button("1/48", "format:1/48"),
Button("1 / 72", "format:1 / 72"),
Button("1ч / 24ч", "format_preset:60:1440"),
Button("1ч / 36ч", "format_preset:60:2160"),
Button("1ч / 48ч", "format_preset:60:2880"),
Button("1ч / 72ч", "format_preset:60:4320"),
),
Row(
Button("1 / (7 дней)", "format:1 / (7 дней)"),
Button("1 / (30 дней)", "format:1 / (30 дней)"),
Button("1 / (без удаления)", "format:1 / (без удаления)"),
Button("1ч / ", "format_preset:60:10080"),
Button("1ч / 30д", "format_preset:60:43200"),
Button("1ч / 60д", "format_preset:60:86400"),
Button("1ч / 90д", "format_preset:60:129600"),
),
Row(
Button("2 / 24", "format:2 / 24"),
Button("2/48", "format:2/48"),
Button("2/72", "format:2/72"),
Button(" / без удаления", "format_preset:60:0"),
),
Row(
Button("Назад", s.backAction()),
Button("2ч / 24ч", "format_preset:120:1440"),
Button("2ч / 36ч", "format_preset:120:2160"),
Button("2ч / 48ч", "format_preset:120:2880"),
Button("2ч / 72ч", "format_preset:120:4320"),
),
Row(
Button("2ч / 7д", "format_preset:120:10080"),
Button("2ч / 30д", "format_preset:120:43200"),
Button("2ч / 60д", "format_preset:120:86400"),
Button("2ч / 90д", "format_preset:120:129600"),
),
Row(
Button("← Назад", s.backAction()),
Button("Свой формат", "format_custom"),
),
)
@@ -725,7 +859,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
}
return
case "format_custom":
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
s.renderCustomFormatTop(b, mode)
return
case "format_custom_top_input":
text += "<b>Введите число для времени в топе</b>\n\n"
unit := "часах"
if s.CustomFormatTopUnit == "minutes" {
unit = "минутах"
}
text += fmt.Sprintf("Единица: <b>%s</b>", unit)
case "format_custom_feed":
s.renderCustomFormatFeed(b, mode)
return
case "format_custom_feed_input":
text += "<b>Введите число для времени в ленте</b>\n\n"
unit := "часах"
if s.CustomFormatFeedUnit == "days" {
unit = "днях"
}
text += fmt.Sprintf("Единица: <b>%s</b>", unit)
case "invite_link_type":
text = "<i>Страница выбора типа ссылки</i>\n\n"
if s.CurrentChannel != "" {
@@ -777,6 +929,127 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
}
}
func (s *PurchaseOptionalDetails) renderCustomFormatTop(b *bot.Bot, mode bot.RenderMode) {
text := "<b>Свой формат — время в топе</b>\n\n"
if s.CurrentChannel != "" {
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
}
var rows [][]echotron.InlineKeyboardButton
if s.CustomFormatTopUnit == "minutes" {
text += "⏱ Единица: <b>минуты</b>\n\n"
text += "Выберите время в топе"
rows = append(rows,
Row(
Button("10", "format_custom_top:10"),
Button("15", "format_custom_top:15"),
Button("20", "format_custom_top:20"),
Button("30", "format_custom_top:30"),
),
Row(
Button("45", "format_custom_top:45"),
Button("60", "format_custom_top:60"),
Button("90", "format_custom_top:90"),
Button("120", "format_custom_top:120"),
),
Row(
Button("↔ Часы", "format_custom_top_toggle_unit"),
Button("Ввести число", "format_custom_top_input_mode"),
),
)
} else {
text += "⏱ Единица: <b>часы</b>\n\n"
text += "Выберите время в топе"
rows = append(rows,
Row(
Button("1", "format_custom_top:60"),
Button("2", "format_custom_top:120"),
Button("3", "format_custom_top:180"),
Button("4", "format_custom_top:240"),
),
Row(
Button("5", "format_custom_top:300"),
Button("6", "format_custom_top:360"),
Button("8", "format_custom_top:480"),
Button("12", "format_custom_top:720"),
),
Row(
Button("↔ Минуты", "format_custom_top_toggle_unit"),
Button("Ввести число", "format_custom_top_input_mode"),
),
)
}
rows = append(rows, Row(Button("← Назад", "format_custom_back_to_select")))
b.Render(text, Keyboard(rows...), mode)
}
func (s *PurchaseOptionalDetails) renderCustomFormatFeed(b *bot.Bot, mode bot.RenderMode) {
text := "<b>Свой формат — время в ленте</b>\n\n"
if s.CurrentChannel != "" {
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
}
if s.CustomFormatTopValue != nil {
text += fmt.Sprintf("Время в топе: <b>%s</b> ✓\n\n", formatDuration(*s.CustomFormatTopValue, "top"))
}
var rows [][]echotron.InlineKeyboardButton
if s.CustomFormatFeedUnit == "days" {
text += "⏱ Единица: <b>дни</b>\n\n"
text += "Выберите время в ленте"
rows = append(rows,
Row(
Button("7", "format_custom_feed:10080"),
Button("14", "format_custom_feed:20160"),
Button("30", "format_custom_feed:43200"),
Button("60", "format_custom_feed:86400"),
),
Row(
Button("90", "format_custom_feed:129600"),
Button("120", "format_custom_feed:172800"),
Button("180", "format_custom_feed:259200"),
Button("365", "format_custom_feed:525600"),
),
Row(
Button("↔ Часы", "format_custom_feed_toggle_unit"),
Button("Без удаления", "format_custom_feed:0"),
),
)
} else {
text += "⏱ Единица: <b>часы</b>\n\n"
text += "Выберите время в ленте"
rows = append(rows,
Row(
Button("24", "format_custom_feed:1440"),
Button("36", "format_custom_feed:2160"),
Button("48", "format_custom_feed:2880"),
Button("72", "format_custom_feed:4320"),
),
Row(
Button("96", "format_custom_feed:5760"),
Button("120", "format_custom_feed:7200"),
Button("144", "format_custom_feed:8640"),
Button("168", "format_custom_feed:10080"),
),
Row(
Button("↔ Дни", "format_custom_feed_toggle_unit"),
Button("Без удаления", "format_custom_feed:0"),
),
)
}
rows = append(rows,
Row(Button("Ввести число", "format_custom_feed_input_mode")),
Row(Button("← Назад", "format_custom_back_to_top")),
)
b.Render(text, Keyboard(rows...), mode)
}
func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMode) {
text := "<b>⚙ Переключить режим параметра</b>\n\n"
@@ -1382,6 +1655,8 @@ func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
for _, ch := range s.Channels {
key := channelKey(ch)
s.FormatByChannel[key] = s.Format
s.TopTimeByChannel[key] = s.TopTimeMinutes
s.FeedTimeByChannel[key] = s.FeedTimeMinutes
}
case "invite_link_type":
if s.InviteLinkType == "" {
@@ -1512,6 +1787,8 @@ func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
delete(s.CommentByChannel, channelKey)
case "format":
delete(s.FormatByChannel, channelKey)
delete(s.TopTimeByChannel, channelKey)
delete(s.FeedTimeByChannel, channelKey)
case "invite_link_type":
delete(s.InviteLinkTypeByChannel, channelKey)
}
@@ -1660,6 +1937,18 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatByChannel == nil {
s.FormatByChannel = make(map[string]string)
}
if s.TopTimeByChannel == nil {
s.TopTimeByChannel = make(map[string]*int)
}
if s.FeedTimeByChannel == nil {
s.FeedTimeByChannel = make(map[string]*int)
}
if s.CustomFormatTopUnit == "" {
s.CustomFormatTopUnit = "hours"
}
if s.CustomFormatFeedUnit == "" {
s.CustomFormatFeedUnit = "hours"
}
if s.InviteLinkTypeByChannel == nil {
s.InviteLinkTypeByChannel = make(map[string]string)
}
@@ -1709,6 +1998,16 @@ func (s *PurchaseOptionalDetails) syncChannelMaps() {
delete(s.FormatByChannel, key)
}
}
for key := range s.TopTimeByChannel {
if _, ok := valid[key]; !ok {
delete(s.TopTimeByChannel, key)
}
}
for key := range s.FeedTimeByChannel {
if _, ok := valid[key]; !ok {
delete(s.FeedTimeByChannel, key)
}
}
for key := range s.InviteLinkTypeByChannel {
if _, ok := valid[key]; !ok {
delete(s.InviteLinkTypeByChannel, key)
@@ -2156,12 +2455,64 @@ func (s *PurchaseOptionalDetails) setCostBeforeValue(value float64) {
s.CostBeforeByChannel[s.CurrentChannel] = entry
}
func formatDuration(minutes int, unit string) string {
switch unit {
case "top":
if minutes > 0 && minutes%60 == 0 {
return fmt.Sprintf("%dч", minutes/60)
}
return fmt.Sprintf("%dмин", minutes)
case "feed":
if minutes == 0 {
return "без удаления"
}
if minutes >= 7*24*60 && minutes%(24*60) == 0 {
return fmt.Sprintf("%dд", minutes/(24*60))
}
if minutes%60 == 0 {
return fmt.Sprintf("%dч", minutes/60)
}
return fmt.Sprintf("%dмин", minutes)
}
return fmt.Sprintf("%d", minutes)
}
func formatLabel(topMinutes, feedMinutes int) string {
return formatDuration(topMinutes, "top") + " / " + formatDuration(feedMinutes, "feed")
}
func (s *PurchaseOptionalDetails) setFormatPreset(topMinutes int, feedMinutes int) {
label := formatLabel(topMinutes, feedMinutes)
topPtr := &topMinutes
var feedPtr *int
if feedMinutes == 0 {
// 0 = без удаления — сохраняем как 0
feedPtr = &feedMinutes
} else {
feedPtr = &feedMinutes
}
if s.CurrentChannel == "" {
s.Format = label
s.TopTimeMinutes = topPtr
s.FeedTimeMinutes = feedPtr
} else {
s.FormatByChannel[s.CurrentChannel] = label
s.TopTimeByChannel[s.CurrentChannel] = topPtr
s.FeedTimeByChannel[s.CurrentChannel] = feedPtr
}
}
func (s *PurchaseOptionalDetails) setFormatValue(value string) {
if s.CurrentChannel == "" {
s.Format = value
s.TopTimeMinutes = nil
s.FeedTimeMinutes = nil
return
}
s.FormatByChannel[s.CurrentChannel] = value
delete(s.TopTimeByChannel, s.CurrentChannel)
delete(s.FeedTimeByChannel, s.CurrentChannel)
}
func (s *PurchaseOptionalDetails) setInviteLinkTypeValue(value string) {
@@ -2314,8 +2665,18 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string, placeme
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
details.Format = &value
}
} else if s.Format != "" {
details.Format = &s.Format
if value, ok := s.TopTimeByChannel[channelKey]; ok && value != nil {
details.TopTimeMinutes = value
}
if value, ok := s.FeedTimeByChannel[channelKey]; ok && value != nil {
details.FeedTimeMinutes = value
}
} else {
if s.Format != "" {
details.Format = &s.Format
}
details.TopTimeMinutes = s.TopTimeMinutes
details.FeedTimeMinutes = s.FeedTimeMinutes
}
// Invite link type
@@ -2351,6 +2712,8 @@ func (s *PurchaseOptionalDetails) isChannelDetailsEmpty(details *backend.Placeme
details.CostBeforeBargain == nil &&
details.PlacementType == nil &&
details.Format == nil &&
details.TopTimeMinutes == nil &&
details.FeedTimeMinutes == nil &&
details.Comment == nil &&
details.InviteLinkType == nil
}