Files
tgex-backend/telegram_bot/screens/purchase_optional_details.go
2026-01-03 17:52:29 +03:00

1194 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package screens
import (
"fmt"
"strconv"
"strings"
"time"
"example.com/m/bot"
"github.com/NicoNex/echotron/v3"
)
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
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
}
text := "<i>… Создание Дополнительно</i>\n\n"
text += "<b>Страница создания закупа и необязательные составляющие</b>"
text += "\n\n"
text += s.formatOptionalSummary()
var rows [][]echotron.InlineKeyboardButton
rows = append(rows, bot.Row(
s.placementButton(),
s.costButton(),
))
rows = append(rows, bot.Row(
s.paymentButton(),
s.typeButton(),
))
rows = append(rows, bot.Row(
s.costBeforeButton(),
s.formatButton(),
))
rows = append(rows, bot.Row(
s.commentButton(),
))
if row := s.costBeforeDeleteRow(); row != nil {
rows = append(rows, row)
}
if row := s.commentDeleteRow(); row != nil {
rows = append(rows, row)
}
rows = append(rows, bot.Row(bot.Button("Назад", "back"), bot.Button("Далее", "next")))
keyboard := bot.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 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{
Title: "Дата оплаты",
Key: "payment_date",
IncludeTime: true,
Selected: s.PaymentDate,
BackState: s,
}), bot.EditMessage)
case "opt_cost":
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":
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"
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_value":
s.InputMode = "cost_value"
s.Enter(b, bot.EditMessage)
case "cost_before":
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
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{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
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.setFormatValue(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 "type":
s.PurchaseType = text
case "format_custom":
s.setFormatValue(text)
case "comment":
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) 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
}
}
}
func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
lines := []string{
fmt.Sprintf("Дата размещения: <b>%s</b>%s", s.formatPlacementSummary(), s.formatPlacementDetails()),
fmt.Sprintf("Дата оплаты: <b>%s</b>", s.formatDateTime(s.PaymentDate)),
fmt.Sprintf("Стоимость: <b>%s</b>%s", s.formatCostSummary(), s.formatCostDetails()),
fmt.Sprintf("Стоимость до торга: <b>%s</b>%s", s.formatCostBeforeSummary(), s.formatCostBeforeDetails()),
fmt.Sprintf("Тип закупа: <b>%s</b>", s.formatText(s.PurchaseType)),
fmt.Sprintf("Формат: <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails()),
fmt.Sprintf("Комментарий: <b>%s</b>", s.formatText(s.Comment)),
}
return strings.Join(lines, "\n")
}
func (s *PurchaseOptionalDetails) formatDateTime(value *time.Time) string {
if value == nil {
return "—"
}
local := value.In(mskLocation)
return fmt.Sprintf("%s %02d %s %s", weekdayName(local.Weekday()), local.Day(), monthShort(local.Month()), local.Format("15:04"))
}
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 {
return "—"
}
return fmt.Sprintf("%.0f₽", *s.CostBeforeBargain)
}
func (s *PurchaseOptionalDetails) costTypeLabel() string {
if s.CostType == "cpm" {
return "СРМ"
}
return "Фикс"
}
func (s *PurchaseOptionalDetails) placementButton() echotron.InlineKeyboardButton {
if !s.hasPlacementValue() {
return bot.Button(" Дата размещения", "opt_datetime")
}
return bot.Button("✎ Дата размещения", "opt_datetime")
}
func (s *PurchaseOptionalDetails) paymentButton() echotron.InlineKeyboardButton {
if s.PaymentDate == nil {
return bot.Button(" Дата оплаты", "opt_payment_date")
}
return bot.Button("✎ Дата оплаты", "opt_payment_date")
}
func (s *PurchaseOptionalDetails) costButton() echotron.InlineKeyboardButton {
if !s.hasCostValue() {
return bot.Button(" Стоимость", "opt_cost")
}
return bot.Button("✎ Стоимость", "opt_cost")
}
func (s *PurchaseOptionalDetails) costBeforeButton() echotron.InlineKeyboardButton {
if !s.hasCostBeforeValue() {
return bot.Button(" До торга", "cost_before")
}
return bot.Button("✎ До торга", "cost_before")
}
func (s *PurchaseOptionalDetails) typeButton() echotron.InlineKeyboardButton {
if s.PurchaseType == "" {
return bot.Button(" Тип", "opt_type")
}
return bot.Button("✎ Тип", "opt_type")
}
func (s *PurchaseOptionalDetails) formatButton() echotron.InlineKeyboardButton {
if !s.hasFormatValue() {
return bot.Button(" Формат", "opt_format")
}
return bot.Button("✎ Формат", "opt_format")
}
func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton {
if s.Comment == "" {
return bot.Button(" Комментарий", "opt_comment")
}
return bot.Button("✎ Комментарий", "opt_comment")
}
func (s *PurchaseOptionalDetails) commentDeleteRow() []echotron.InlineKeyboardButton {
if s.Comment == "" {
return nil
}
return bot.Row(bot.Button("⌫ Удалить комментарий", "delete_comment"))
}
func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboardButton {
if s.CostBeforeBargain == nil || s.CostBeforeMode == "per_channel" {
return nil
}
return bot.Row(bot.Button("⌫ Удалить до торга", "delete_cost_before"))
}
func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderMode) {
text := "<i>… Создание Дополнительно</i>\n\n"
switch s.InputMode {
case "type":
text += "<b>Введите тип закупа</b>\n\nНапример: <code>вп</code> или <code>стандарт</code>"
case "cost_select":
text = "<i>Страница ввода стоимости</i>\n\n"
if s.CurrentChannel != "" {
text += fmt.Sprintf("Канал: <b>@%s</b>\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.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)
} 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>", s.CurrentChannel)
}
case "cost_before_value":
text += "<b>Ввод стоимости до торга</b>\n\nНапример: <code>20000</code>"
if s.CurrentChannel != "" {
text += fmt.Sprintf("\n\nКанал: <b>@%s</b>", s.CurrentChannel)
}
case "format_select":
text = "<i>Страница ввода формата размещения</i>\n\n"
if s.CurrentChannel != "" {
text += fmt.Sprintf("Канал: <b>@%s</b>\n\n", s.CurrentChannel)
}
text += "Выберите формат\n\n"
keyboard := bot.Keyboard(
bot.Row(
bot.Button("1 / 24", "format:1 / 24"),
bot.Button("1/48", "format:1/48"),
bot.Button("1 / 72", "format:1 / 72"),
),
bot.Row(
bot.Button("1 / (7 дней)", "format:1 / (7 дней)"),
bot.Button("1 / (30 дней)", "format:1 / (30 дней)"),
bot.Button("1 / (без удаления)", "format:1 / (без удаления)"),
),
bot.Row(
bot.Button("2 / 24", "format:2 / 24"),
bot.Button("2/48", "format:2/48"),
bot.Button("2/72", "format:2/72"),
),
bot.Row(
bot.Button("Назад", s.backAction()),
bot.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 "comment":
text += "<b>Введите комментарий</b>"
default:
s.InputMode = ""
s.Enter(b, mode)
return
}
keyboard := bot.Keyboard(
bot.Row(bot.Button("← Назад", s.backAction())),
)
if mode == bot.EditMessage {
b.Edit(text, keyboard)
} else {
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 := "<i>… Создание Дополнительно</i>\n\n"
text += fmt.Sprintf("<b>%s</b>\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 := "<i>… Создание Дополнительно</i>\n\n"
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, 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"
}