diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go
index 04347ce..9b9b28c 100644
--- a/telegram_bot/screens/purchase_optional_details.go
+++ b/telegram_bot/screens/purchase_optional_details.go
@@ -11,26 +11,51 @@ import (
)
type PurchaseOptionalDetails struct {
- WorkspaceID string
- ProjectID string
- ProjectTitle string
- CreativeID string
- CreativeTitle string
- Channels []PurchaseChannelInput
- PlacementDateTime *time.Time
- PaymentDate *time.Time
- CostType string
- CostValue *float64
- CostBeforeBargain *float64
- PurchaseType string
- Format string
- Comment string
- InputMode string
- BackState bot.State
+ WorkspaceID string
+ ProjectID string
+ ProjectTitle string
+ CreativeID string
+ CreativeTitle string
+ Channels []PurchaseChannelInput
+ PlacementDateTime *time.Time
+ PaymentDate *time.Time
+ CostType string
+ CostValue *float64
+ CostBeforeBargain *float64
+ PurchaseType string
+ Format string
+ Comment string
+ InputMode string
+ CurrentParam string
+ CurrentChannel string
+ ParamPage int
+ ReturnMode string
+ PlacementMode string
+ CostMode string
+ CostBeforeMode string
+ FormatMode string
+ PlacementByChannel map[string]*time.Time
+ CostByChannel map[string]CostEntry
+ CostBeforeByChannel map[string]*float64
+ FormatByChannel map[string]string
+ PlacementCopy *time.Time
+ CostCopy *CostEntry
+ CostBeforeCopy *float64
+ FormatCopy *string
+ 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.renderParamScreens(b, mode) {
+ return
+ }
s.renderInputPrompt(b, mode)
return
}
@@ -74,16 +99,17 @@ 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":
- b.SetState(NewDateTimePicker(DateTimePickerConfig{
- Title: "Дата и время размещения",
- Key: "placement_datetime",
- IncludeTime: true,
- Selected: s.PlacementDateTime,
- BackState: s,
- }), bot.EditMessage)
+ if len(s.Channels) > 1 {
+ s.InputMode = "placement_scope"
+ s.Enter(b, bot.EditMessage)
+ } else {
+ s.setParamMode("placement", "common")
+ s.openCommonEditor(b, "placement")
+ }
case "opt_payment_date":
b.SetState(NewDateTimePicker(DateTimePickerConfig{
@@ -95,16 +121,26 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
}), bot.EditMessage)
case "opt_cost":
- s.InputMode = "cost_select"
- s.Enter(b, bot.EditMessage)
+ if len(s.Channels) > 1 {
+ s.InputMode = "cost_scope"
+ s.Enter(b, bot.EditMessage)
+ } else {
+ s.setParamMode("cost", "common")
+ s.openCommonEditor(b, "cost")
+ }
case "opt_type":
s.InputMode = "type"
s.Enter(b, bot.EditMessage)
case "opt_format":
- s.InputMode = "format_select"
- s.Enter(b, bot.EditMessage)
+ if len(s.Channels) > 1 {
+ s.InputMode = "format_scope"
+ s.Enter(b, bot.EditMessage)
+ } else {
+ s.setParamMode("format", "common")
+ s.openCommonEditor(b, "format")
+ }
case "opt_comment":
s.InputMode = "comment"
@@ -119,11 +155,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage)
case "cost_type_toggle":
- if s.costTypeLabel() == "СРМ" {
- s.CostType = "fixed"
- } else {
- s.CostType = "cpm"
- }
+ s.toggleCostType()
s.Enter(b, bot.EditMessage)
case "cost_value":
@@ -131,8 +163,13 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage)
case "cost_before":
- s.InputMode = "cost_before"
- s.Enter(b, bot.EditMessage)
+ if len(s.Channels) > 1 {
+ s.InputMode = "cost_before_scope"
+ s.Enter(b, bot.EditMessage)
+ } else {
+ s.setParamMode("cost_before", "common")
+ s.openCommonEditor(b, "cost_before")
+ }
case "delete_cost_before":
s.CostBeforeBargain = nil
@@ -140,6 +177,18 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
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":
@@ -153,10 +202,19 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
BackState: s.BackState,
}, bot.EditMessage)
default:
+ if s.handleParamCallback(b, u.CallbackQuery.Data) {
+ return
+ }
if strings.HasPrefix(u.CallbackQuery.Data, "format:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "format:")
- s.Format = value
- s.InputMode = ""
+ s.setFormatValue(value)
+ if s.ReturnMode != "" {
+ s.InputMode = s.ReturnMode
+ s.ReturnMode = ""
+ } else {
+ s.InputMode = ""
+ }
+ s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
return
}
@@ -168,6 +226,7 @@ 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 == "" {
@@ -178,7 +237,7 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
case "type":
s.PurchaseType = text
case "format_custom":
- s.Format = text
+ s.setFormatValue(text)
case "comment":
s.Comment = text
case "cost_value":
@@ -187,17 +246,23 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
s.renderInputPrompt(b, bot.EditMessage)
return
}
- s.CostValue = &value
- case "cost_before":
+ 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.CostBeforeBargain = &value
+ s.setCostBeforeValue(value)
}
- s.InputMode = ""
+ if s.ReturnMode != "" {
+ s.InputMode = s.ReturnMode
+ s.ReturnMode = ""
+ } else {
+ s.InputMode = ""
+ }
+ s.CurrentChannel = ""
s.Enter(b, bot.NewMessage)
}
@@ -209,17 +274,23 @@ func (s *PurchaseOptionalDetails) SetDateTimeSelection(key string, value time.Ti
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
+ }
}
}
func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
lines := []string{
- fmt.Sprintf("Дата размещения: %s", s.formatDateTime(s.PlacementDateTime)),
+ fmt.Sprintf("Дата размещения: %s%s", s.formatPlacementSummary(), s.formatPlacementDetails()),
fmt.Sprintf("Дата оплаты: %s", s.formatDateTime(s.PaymentDate)),
- fmt.Sprintf("Стоимость: %s", s.formatCostValue()),
- fmt.Sprintf("Стоимость до торга: %s", s.formatCostBefore()),
+ fmt.Sprintf("Стоимость: %s%s", s.formatCostSummary(), s.formatCostDetails()),
+ fmt.Sprintf("Стоимость до торга: %s%s", s.formatCostBeforeSummary(), s.formatCostBeforeDetails()),
fmt.Sprintf("Тип закупа: %s", s.formatText(s.PurchaseType)),
- fmt.Sprintf("Формат: %s", s.formatText(s.Format)),
+ fmt.Sprintf("Формат: %s%s", s.formatFormatSummary(), s.formatFormatDetails()),
fmt.Sprintf("Комментарий: %s", s.formatText(s.Comment)),
}
return strings.Join(lines, "\n")
@@ -262,7 +333,7 @@ func (s *PurchaseOptionalDetails) costTypeLabel() string {
}
func (s *PurchaseOptionalDetails) placementButton() echotron.InlineKeyboardButton {
- if s.PlacementDateTime == nil {
+ if !s.hasPlacementValue() {
return bot.Button("+ Дата размещения", "opt_datetime")
}
return bot.Button("✎ Дата размещения", "opt_datetime")
@@ -276,14 +347,14 @@ func (s *PurchaseOptionalDetails) paymentButton() echotron.InlineKeyboardButton
}
func (s *PurchaseOptionalDetails) costButton() echotron.InlineKeyboardButton {
- if s.CostValue == nil {
+ if !s.hasCostValue() {
return bot.Button("+ Стоимость", "opt_cost")
}
return bot.Button("✎ Стоимость", "opt_cost")
}
func (s *PurchaseOptionalDetails) costBeforeButton() echotron.InlineKeyboardButton {
- if s.CostBeforeBargain == nil {
+ if !s.hasCostBeforeValue() {
return bot.Button("+ До торга", "cost_before")
}
return bot.Button("✎ До торга", "cost_before")
@@ -297,7 +368,7 @@ func (s *PurchaseOptionalDetails) typeButton() echotron.InlineKeyboardButton {
}
func (s *PurchaseOptionalDetails) formatButton() echotron.InlineKeyboardButton {
- if s.Format == "" {
+ if !s.hasFormatValue() {
return bot.Button("+ Формат", "opt_format")
}
return bot.Button("✎ Формат", "opt_format")
@@ -318,7 +389,7 @@ func (s *PurchaseOptionalDetails) commentDeleteRow() []echotron.InlineKeyboardBu
}
func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboardButton {
- if s.CostBeforeBargain == nil {
+ if s.CostBeforeBargain == nil || s.CostBeforeMode == "per_channel" {
return nil
}
return bot.Row(bot.Button("⌫ Удалить до торга", "delete_cost_before"))
@@ -331,11 +402,18 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
text += "Введите тип закупа\n\nНапример: вп или стандарт"
case "cost_select":
text = "Страница ввода стоимости\n\n"
+ if s.CurrentChannel != "" {
+ text += fmt.Sprintf("Канал: @%s\n\n", s.CurrentChannel)
+ }
text += "Ставка СРМ / Фикс цена (по умолчанию фикс)\n\n"
+ costValue := s.formatCostValue()
+ if s.CurrentChannel != "" {
+ costValue = s.formatCostForChannel(s.CurrentChannel)
+ }
keyboard := bot.Keyboard(
- bot.Row(bot.Button(fmt.Sprintf("Тип: %s", s.costTypeLabel()), "cost_type_toggle")),
- bot.Row(bot.Button(fmt.Sprintf("Стоимость: %s", s.formatCostValue()), "cost_value")),
- bot.Row(bot.Button("Назад", "back_to_optional")),
+ bot.Row(bot.Button(fmt.Sprintf("Тип: %s", s.costTypeLabelForCurrent()), "cost_type_toggle")),
+ bot.Row(bot.Button(fmt.Sprintf("Стоимость: %s", costValue), "cost_value")),
+ bot.Row(bot.Button("Назад", s.backAction())),
)
if mode == bot.EditMessage {
b.Edit(text, keyboard)
@@ -345,10 +423,19 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
return
case "cost_value":
text += "Ввод стоимости (СРМ / фикс)\n\nНапример: 15000"
- case "cost_before":
+ if s.CurrentChannel != "" {
+ text += fmt.Sprintf("\n\nКанал: @%s", s.CurrentChannel)
+ }
+ case "cost_before_value":
text += "Ввод стоимости до торга\n\nНапример: 20000"
+ if s.CurrentChannel != "" {
+ text += fmt.Sprintf("\n\nКанал: @%s", s.CurrentChannel)
+ }
case "format_select":
text = "Страница ввода формата размещения\n\n"
+ if s.CurrentChannel != "" {
+ text += fmt.Sprintf("Канал: @%s\n\n", s.CurrentChannel)
+ }
text += "Выберите формат\n\n"
keyboard := bot.Keyboard(
bot.Row(
@@ -367,7 +454,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
bot.Button("2/72", "format:2/72"),
),
bot.Row(
- bot.Button("Назад", "back_to_optional"),
+ bot.Button("Назад", s.backAction()),
bot.Button("Свой формат", "format_custom"),
),
)
@@ -388,7 +475,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
}
keyboard := bot.Keyboard(
- bot.Row(bot.Button("← Назад", "back_to_optional")),
+ bot.Row(bot.Button("← Назад", s.backAction())),
)
if mode == bot.EditMessage {
@@ -397,3 +484,710 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
b.SendNew(text, keyboard)
}
}
+
+func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.RenderMode) bool {
+ switch s.InputMode {
+ case "placement_scope":
+ s.renderParamScope(b, mode, "placement")
+ return true
+ case "placement_channels":
+ s.renderParamChannels(b, mode, "placement")
+ return true
+ case "cost_scope":
+ s.renderParamScope(b, mode, "cost")
+ return true
+ case "cost_channels":
+ s.renderParamChannels(b, mode, "cost")
+ return true
+ case "cost_before_scope":
+ s.renderParamScope(b, mode, "cost_before")
+ return true
+ case "cost_before_channels":
+ s.renderParamChannels(b, mode, "cost_before")
+ return true
+ case "format_scope":
+ s.renderParamScope(b, mode, "format")
+ return true
+ case "format_channels":
+ s.renderParamChannels(b, mode, "format")
+ return true
+ default:
+ return false
+ }
+}
+
+func (s *PurchaseOptionalDetails) renderParamScope(b *bot.Bot, mode bot.RenderMode, param string) {
+ title := s.paramTitle(param)
+ text := "… › Создание › Дополнительно\n\n"
+ text += fmt.Sprintf("%s\n\n", title)
+ text += "Выберите режим\n\n"
+
+ commonLabel := "Общий"
+ perLabel := "Для каждого"
+ if s.paramMode(param) == "common" {
+ commonLabel = "● " + commonLabel
+ } else {
+ perLabel = "● " + perLabel
+ }
+
+ rows := [][]echotron.InlineKeyboardButton{
+ bot.Row(
+ bot.Button(commonLabel, fmt.Sprintf("param_mode_common:%s", param)),
+ bot.Button(perLabel, fmt.Sprintf("param_mode_per:%s", param)),
+ ),
+ bot.Row(bot.Button("← Назад", "back_to_optional")),
+ }
+
+ b.Render(text, bot.Keyboard(rows...), mode)
+}
+
+func (s *PurchaseOptionalDetails) renderParamChannels(b *bot.Bot, mode bot.RenderMode, param string) {
+ s.CurrentParam = param
+ text := "… › Создание › Дополнительно\n\n"
+ text += fmt.Sprintf("%s — по каналам\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, bot.Keyboard(bot.Row(bot.Button("← Назад", "back_to_optional"))), mode)
+ return
+ }
+
+ start := s.ParamPage * perPage
+ if start >= total {
+ s.ParamPage = 0
+ start = 0
+ }
+ end := start + perPage
+ if end > total {
+ end = total
+ }
+
+ var rows [][]echotron.InlineKeyboardButton
+ for i := start; i < end; i++ {
+ label := fmt.Sprintf("%d", i+1)
+ rows = append(rows, bot.Row(
+ bot.Button(label, fmt.Sprintf("param_edit:%s:%d", param, i)),
+ bot.Button("⎘", fmt.Sprintf("param_copy:%s:%d", param, i)),
+ bot.Button("⤵", fmt.Sprintf("param_paste:%s:%d", param, i)),
+ bot.Button("✎", fmt.Sprintf("param_edit:%s:%d", param, i)),
+ bot.Button("⌫", fmt.Sprintf("param_clear:%s:%d", param, i)),
+ ))
+ }
+
+ pages := (total + perPage - 1) / perPage
+ if pages > 1 {
+ var navRow []echotron.InlineKeyboardButton
+ if s.ParamPage > 0 {
+ navRow = append(navRow, bot.Button("←", "param_prev"))
+ } else {
+ navRow = append(navRow, bot.Button("·", "empty"))
+ }
+ navRow = append(navRow, bot.Button(fmt.Sprintf("• %d/%d •", s.ParamPage+1, pages), "page_info"))
+ if s.ParamPage+1 < pages {
+ navRow = append(navRow, bot.Button("→", "param_next"))
+ } else {
+ navRow = append(navRow, bot.Button("·", "empty"))
+ }
+ rows = append(rows, navRow)
+ }
+
+ rows = append(rows, bot.Row(bot.Button("Применить ко всем", fmt.Sprintf("param_apply_all:%s", param))))
+ rows = append(rows, bot.Row(
+ bot.Button("← Назад", fmt.Sprintf("param_back:%s", param)),
+ ))
+
+ b.Render(text, bot.Keyboard(rows...), mode)
+}
+
+func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string {
+ var lines []string
+ for i, ch := range s.Channels {
+ value := s.formatParamValue(param, ch.Username)
+ lines = append(lines, fmt.Sprintf("%d. @%s — %s", i+1, ch.Username, value))
+ }
+ return strings.Join(lines, "\n")
+}
+
+func (s *PurchaseOptionalDetails) handleParamCallback(b *bot.Bot, data string) bool {
+ switch {
+ case strings.HasPrefix(data, "param_mode_common:"):
+ param := strings.TrimPrefix(data, "param_mode_common:")
+ s.setParamMode(param, "common")
+ s.CurrentChannel = ""
+ s.CurrentParam = param
+ s.openCommonEditor(b, param)
+ return true
+ case strings.HasPrefix(data, "param_mode_per:"):
+ param := strings.TrimPrefix(data, "param_mode_per:")
+ s.setParamMode(param, "per_channel")
+ s.CurrentParam = param
+ s.InputMode = param + "_channels"
+ s.Enter(b, bot.EditMessage)
+ return true
+ case strings.HasPrefix(data, "param_edit:"):
+ param, index, ok := s.parseParamIndex(data, "param_edit:")
+ if !ok {
+ return false
+ }
+ if index >= 0 && index < len(s.Channels) {
+ s.CurrentParam = param
+ s.CurrentChannel = s.Channels[index].Username
+ s.openChannelEditor(b, param, s.CurrentChannel)
+ return true
+ }
+ case strings.HasPrefix(data, "param_copy:"):
+ param, index, ok := s.parseParamIndex(data, "param_copy:")
+ if !ok {
+ return false
+ }
+ if index >= 0 && index < len(s.Channels) {
+ username := s.Channels[index].Username
+ s.copyParamValue(param, username)
+ s.Enter(b, bot.EditMessage)
+ return true
+ }
+ case strings.HasPrefix(data, "param_paste:"):
+ param, index, ok := s.parseParamIndex(data, "param_paste:")
+ if !ok {
+ return false
+ }
+ if index >= 0 && index < len(s.Channels) {
+ username := s.Channels[index].Username
+ s.pasteParamValue(param, username)
+ s.Enter(b, bot.EditMessage)
+ return true
+ }
+ case strings.HasPrefix(data, "param_clear:"):
+ param, index, ok := s.parseParamIndex(data, "param_clear:")
+ if !ok {
+ return false
+ }
+ if index >= 0 && index < len(s.Channels) {
+ username := s.Channels[index].Username
+ s.clearParamValue(param, username)
+ s.Enter(b, bot.EditMessage)
+ return true
+ }
+ case strings.HasPrefix(data, "param_apply_all:"):
+ param := strings.TrimPrefix(data, "param_apply_all:")
+ s.applyParamToAll(param)
+ s.Enter(b, bot.EditMessage)
+ return true
+ case data == "param_prev":
+ if s.ParamPage > 0 {
+ s.ParamPage--
+ }
+ s.Enter(b, bot.EditMessage)
+ return true
+ case data == "param_next":
+ s.ParamPage++
+ s.Enter(b, bot.EditMessage)
+ return true
+ case strings.HasPrefix(data, "param_back:"):
+ param := strings.TrimPrefix(data, "param_back:")
+ s.InputMode = param + "_scope"
+ 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 := parseInt(parts[1])
+ return param, index, true
+}
+
+func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
+ switch param {
+ case "placement":
+ s.InputMode = ""
+ s.ReturnMode = ""
+ b.SetState(NewDateTimePicker(DateTimePickerConfig{
+ Title: "Дата и время размещения",
+ Key: "placement_datetime",
+ IncludeTime: true,
+ Selected: s.PlacementDateTime,
+ BackState: s,
+ }), bot.EditMessage)
+ case "cost":
+ s.InputMode = "cost_select"
+ 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)
+ default:
+ s.InputMode = ""
+ s.Enter(b, bot.EditMessage)
+ }
+}
+
+func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, username string) {
+ switch param {
+ case "placement":
+ s.InputMode = "placement_channels"
+ b.SetState(NewDateTimePicker(DateTimePickerConfig{
+ Title: "Дата и время размещения",
+ Key: "placement_datetime:" + username,
+ IncludeTime: true,
+ Selected: s.PlacementByChannel[username],
+ BackState: s,
+ }), bot.EditMessage)
+ case "cost":
+ s.InputMode = "cost_select"
+ s.ReturnMode = "cost_channels"
+ s.CurrentChannel = username
+ s.Enter(b, bot.EditMessage)
+ case "cost_before":
+ s.InputMode = "cost_before_value"
+ s.ReturnMode = "cost_before_channels"
+ s.CurrentChannel = username
+ s.Enter(b, bot.EditMessage)
+ case "format":
+ s.InputMode = "format_select"
+ s.ReturnMode = "format_channels"
+ s.CurrentChannel = username
+ s.Enter(b, bot.EditMessage)
+ default:
+ s.InputMode = ""
+ s.Enter(b, bot.EditMessage)
+ }
+}
+
+func (s *PurchaseOptionalDetails) copyParamValue(param, username string) {
+ switch param {
+ case "placement":
+ s.PlacementCopy = s.PlacementByChannel[username]
+ case "cost":
+ entry := s.CostByChannel[username]
+ copied := entry
+ if entry.Value == nil {
+ copied.Value = nil
+ } else {
+ value := *entry.Value
+ copied.Value = &value
+ }
+ s.CostCopy = &copied
+ case "cost_before":
+ s.CostBeforeCopy = s.CostBeforeByChannel[username]
+ case "format":
+ if value, ok := s.FormatByChannel[username]; ok {
+ copied := value
+ s.FormatCopy = &copied
+ } else {
+ s.FormatCopy = nil
+ }
+ }
+}
+
+func (s *PurchaseOptionalDetails) pasteParamValue(param, username string) {
+ switch param {
+ case "placement":
+ if s.PlacementCopy == nil {
+ s.PlacementByChannel[username] = nil
+ return
+ }
+ value := s.PlacementCopy.In(mskLocation)
+ s.PlacementByChannel[username] = &value
+ case "cost":
+ if s.CostCopy == nil {
+ delete(s.CostByChannel, username)
+ return
+ }
+ copied := *s.CostCopy
+ if copied.Value != nil {
+ value := *copied.Value
+ copied.Value = &value
+ }
+ s.CostByChannel[username] = copied
+ case "cost_before":
+ if s.CostBeforeCopy == nil {
+ s.CostBeforeByChannel[username] = nil
+ return
+ }
+ value := *s.CostBeforeCopy
+ s.CostBeforeByChannel[username] = &value
+ case "format":
+ if s.FormatCopy == nil {
+ delete(s.FormatByChannel, username)
+ return
+ }
+ s.FormatByChannel[username] = *s.FormatCopy
+ }
+}
+
+func (s *PurchaseOptionalDetails) applyParamToAll(param string) {
+ switch param {
+ case "placement":
+ for _, ch := range s.Channels {
+ s.pasteParamValue(param, ch.Username)
+ }
+ case "cost":
+ for _, ch := range s.Channels {
+ s.pasteParamValue(param, ch.Username)
+ }
+ case "cost_before":
+ for _, ch := range s.Channels {
+ s.pasteParamValue(param, ch.Username)
+ }
+ case "format":
+ for _, ch := range s.Channels {
+ s.pasteParamValue(param, ch.Username)
+ }
+ }
+}
+
+func (s *PurchaseOptionalDetails) clearParamValue(param, username string) {
+ switch param {
+ case "placement":
+ s.PlacementByChannel[username] = nil
+ case "cost":
+ delete(s.CostByChannel, username)
+ case "cost_before":
+ s.CostBeforeByChannel[username] = nil
+ case "format":
+ delete(s.FormatByChannel, username)
+ }
+}
+
+func (s *PurchaseOptionalDetails) paramTitle(param string) string {
+ switch param {
+ case "placement":
+ return "Дата и время размещения"
+ case "cost":
+ return "Стоимость"
+ case "cost_before":
+ return "Стоимость до торга"
+ case "format":
+ return "Формат"
+ default:
+ return ""
+ }
+}
+
+func (s *PurchaseOptionalDetails) paramMode(param string) string {
+ switch param {
+ case "placement":
+ if s.PlacementMode == "" {
+ return "common"
+ }
+ return s.PlacementMode
+ case "cost":
+ if s.CostMode == "" {
+ return "common"
+ }
+ return s.CostMode
+ case "cost_before":
+ if s.CostBeforeMode == "" {
+ return "common"
+ }
+ return s.CostBeforeMode
+ case "format":
+ if s.FormatMode == "" {
+ return "common"
+ }
+ return s.FormatMode
+ default:
+ return "common"
+ }
+}
+
+func (s *PurchaseOptionalDetails) setParamMode(param, mode string) {
+ switch param {
+ case "placement":
+ s.PlacementMode = mode
+ case "cost":
+ s.CostMode = mode
+ case "cost_before":
+ s.CostBeforeMode = mode
+ case "format":
+ s.FormatMode = mode
+ }
+}
+
+func (s *PurchaseOptionalDetails) ensureDefaults() {
+ if s.PlacementMode == "" {
+ s.PlacementMode = "common"
+ }
+ if s.CostMode == "" {
+ s.CostMode = "common"
+ }
+ if s.CostBeforeMode == "" {
+ s.CostBeforeMode = "common"
+ }
+ if s.FormatMode == "" {
+ s.FormatMode = "common"
+ }
+ if s.PlacementByChannel == nil {
+ s.PlacementByChannel = make(map[string]*time.Time)
+ }
+ if s.CostByChannel == nil {
+ s.CostByChannel = make(map[string]CostEntry)
+ }
+ if s.CostBeforeByChannel == nil {
+ s.CostBeforeByChannel = make(map[string]*float64)
+ }
+ if s.FormatByChannel == nil {
+ s.FormatByChannel = make(map[string]string)
+ }
+ s.syncChannelMaps()
+}
+
+func (s *PurchaseOptionalDetails) syncChannelMaps() {
+ valid := make(map[string]struct{}, len(s.Channels))
+ for _, ch := range s.Channels {
+ valid[ch.Username] = struct{}{}
+ }
+ for key := range s.PlacementByChannel {
+ if _, ok := valid[key]; !ok {
+ delete(s.PlacementByChannel, 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.FormatByChannel {
+ if _, ok := valid[key]; !ok {
+ delete(s.FormatByChannel, 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 "\n" + s.renderParamChannelSummary("placement")
+}
+
+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 "\n" + 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 "\n" + s.renderParamChannelSummary("cost_before")
+}
+
+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 "\n" + s.renderParamChannelSummary("format")
+}
+
+func (s *PurchaseOptionalDetails) formatParamValue(param, username string) string {
+ switch param {
+ case "placement":
+ value := s.PlacementByChannel[username]
+ return s.formatDateTime(value)
+ case "cost":
+ return s.formatCostForChannel(username)
+ case "cost_before":
+ return s.formatCostBeforeForChannel(username)
+ case "format":
+ if value, ok := s.FormatByChannel[username]; ok && value != "" {
+ return value
+ }
+ return "—"
+ default:
+ return "—"
+ }
+}
+
+func (s *PurchaseOptionalDetails) formatCostForChannel(username string) string {
+ entry, ok := s.CostByChannel[username]
+ if !ok || entry.Value == nil {
+ return "—"
+ }
+ label := s.costTypeLabelForEntry(entry)
+ return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
+}
+
+func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(username string) string {
+ value, ok := s.CostBeforeByChannel[username]
+ if !ok || value == nil {
+ return "—"
+ }
+ return fmt.Sprintf("%.0f₽", *value)
+}
+
+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) 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 _, value := range s.CostBeforeByChannel {
+ if value != nil {
+ return true
+ }
+ }
+ return false
+ }
+ return s.CostBeforeBargain != 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) costTypeLabelForEntry(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) 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) 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 == "" {
+ s.CostBeforeBargain = &value
+ return
+ }
+ s.CostBeforeByChannel[s.CurrentChannel] = &value
+}
+
+func (s *PurchaseOptionalDetails) setFormatValue(value string) {
+ if s.CurrentChannel == "" {
+ s.Format = value
+ return
+ }
+ s.FormatByChannel[s.CurrentChannel] = value
+}
+
+func (s *PurchaseOptionalDetails) backAction() string {
+ if s.ReturnMode != "" {
+ return "back_to_return"
+ }
+ return "back_to_optional"
+}
diff --git a/telegram_bot/screens/select_channels_for_purchase.go b/telegram_bot/screens/select_channels_for_purchase.go
index a490224..93bb92d 100644
--- a/telegram_bot/screens/select_channels_for_purchase.go
+++ b/telegram_bot/screens/select_channels_for_purchase.go
@@ -13,14 +13,9 @@ import (
)
type PurchaseChannelInput struct {
- Username string
- PlannedCost *float64
- Comment *string
- PlacementDateTime *time.Time
- CostType string
- CostValue *float64
- CostBeforeBargain *float64
- Format string
+ Username string
+ PlannedCost *float64
+ Comment *string
}
type SelectChannelsForPurchase struct {