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 := "… › Создание › Дополнительно\n\n" text += "Страница создания закупа и необязательные составляющие" 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("Дата размещения: %s%s", s.formatPlacementSummary(), s.formatPlacementDetails()), fmt.Sprintf("Дата оплаты: %s", s.formatDateTime(s.PaymentDate)), 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", s.formatFormatSummary(), s.formatFormatDetails()), fmt.Sprintf("Комментарий: %s", 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 := "… › Создание › Дополнительно\n\n" switch s.InputMode { case "type": 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.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 += "Ввод стоимости (СРМ / фикс)\n\nНапример: 15000" 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( 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 += "Введите формат\n\nНапример: пост" case "comment": text += "Введите комментарий" 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 := "… › Создание › Дополнительно\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" }