diff --git a/telegram_bot/screens/add_creative.go b/telegram_bot/screens/add_creative.go index bba187a..fc9dfd5 100644 --- a/telegram_bot/screens/add_creative.go +++ b/telegram_bot/screens/add_creative.go @@ -24,8 +24,6 @@ type AddCreative struct { } const msgWaitCreative = ` -… › Креативы › Добавление - + Добавить креатив 📄 Пришлите креатив который хотите добавить diff --git a/telegram_bot/screens/add_project.go b/telegram_bot/screens/add_project.go index bb46f1e..3f3460b 100644 --- a/telegram_bot/screens/add_project.go +++ b/telegram_bot/screens/add_project.go @@ -31,6 +31,7 @@ type AddProject struct { func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) { buttons := [][]echotron.InlineKeyboardButton{ Row(Button("← Назад", "back")), + Row(URLButton("Добавить", "tg://resolve?domain=chat_cruiser_bot&startchannel&admin=invite_users+post_messages+edit_messages+delete_messages")), } keyboard := Keyboard(buttons...) diff --git a/telegram_bot/screens/add_purchase.go b/telegram_bot/screens/add_purchase.go index 8375af1..ee3c738 100644 --- a/telegram_bot/screens/add_purchase.go +++ b/telegram_bot/screens/add_purchase.go @@ -40,7 +40,7 @@ func (s *AddPurchase) Enter(b *bot.Bot, mode bot.RenderMode) { } func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode, jwt string) { - text := "… › Закупы › Создание\n\nСоздание закупа\n\n" + text := "Создание закупа\n\n" text += "Выберите проект и креатив.\n\n" var buttons [][]echotron.InlineKeyboardButton diff --git a/telegram_bot/screens/creative_details.go b/telegram_bot/screens/creative_details.go index 93d7d87..cbac406 100644 --- a/telegram_bot/screens/creative_details.go +++ b/telegram_bot/screens/creative_details.go @@ -24,15 +24,7 @@ type CreativeDetails struct { } func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) { - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", Keyboard()) - return - } - - // Загружаем креатив - creative, err := b.Backend.GetCreative(context.Background(), jwt, s.WorkspaceID, s.CreativeID) + creative, err := b.Backend.GetCreative(context.Background(), b.Session.JWT, s.WorkspaceID, s.CreativeID) if err != nil { log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative") b.SendNew("❌ Не удалось загрузить креатив", Keyboard( @@ -476,25 +468,9 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) { } func (s *CreativeDetails) deleteCreative(b *bot.Bot) { - // JWT уже создан в Bot.Update(), просто берем из сессии - jwt := b.Session.JWT - if jwt == "" { - log.Error().Msg("JWT is empty in session") - b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start", Keyboard()) - return - } - - // Отправляем запрос на удаление - err := b.Backend.DeleteCreative( - context.Background(), - jwt, - s.WorkspaceID, - s.CreativeID, - ) + err := b.Backend.DeleteCreative(context.Background(), b.Session.JWT, s.WorkspaceID, s.CreativeID) if err != nil { - log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to delete creative") - // Показываем ошибку через панель управления s.confirmingDelete = false // Сбрасываем флаг подтверждения s.ShowControlPanel(b, "❌ Не удалось удалить креатив\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{ Row(Button("← Назад", "back")), @@ -503,9 +479,6 @@ func (s *CreativeDetails) deleteCreative(b *bot.Bot) { return } - log.Info().Str("creative_id", s.CreativeID).Msg("Creative deleted successfully") - - // Возвращаемся назад if s.BackState != nil { b.SendNew("✅ Креатив успешно удален!", Keyboard()) b.SetState(s.BackState, bot.EditMessage) diff --git a/telegram_bot/screens/creatives.go b/telegram_bot/screens/creatives.go index d56fd43..557f651 100644 --- a/telegram_bot/screens/creatives.go +++ b/telegram_bot/screens/creatives.go @@ -14,11 +14,10 @@ import ( const creativesPerPage = 6 type Creatives struct { - CurrentPage int - ProjectID string - ProjectTitle string - WorkspaceID string - BackState bot.State + CurrentPage int + ProjectID string + WorkspaceID string + BackState bot.State } func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) { @@ -52,16 +51,8 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) var text string var buttons [][]echotron.InlineKeyboardButton - // Навигация: 2 последних уровня - navPath := "… › Креативы" - if s.ProjectTitle != "" { - navPath = fmt.Sprintf("… › %s › Креативы", s.ProjectTitle) - } - if len(page.Items) == 0 { - text = fmt.Sprintf(`%s - -Креативы + text = `Креативы У вас пока нет креативов @@ -71,16 +62,13 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) ‣ Шаблон для автопостинга ‣ Варианты для A/B тестирования -Начните с добавления первого креатива`, navPath) +Начните с добавления первого креатива` } else { - text = fmt.Sprintf(`%s - -Креативы%s + text = fmt.Sprintf(`Креативы%s Управление рекламными материалами Выберите креатив`, - navPath, ui.FormatPageInfo(s.CurrentPage, page.Pages)) } diff --git a/telegram_bot/screens/login.go b/telegram_bot/screens/login.go index 03ad136..5bf6837 100644 --- a/telegram_bot/screens/login.go +++ b/telegram_bot/screens/login.go @@ -12,7 +12,7 @@ type Login struct{} const msgHelloLogin = ` Привет, %s! -👇Нажми кнопку для входа на сайт. +Нажми кнопку для входа на сайт. 👇 ` func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) { diff --git a/telegram_bot/screens/my_projects.go b/telegram_bot/screens/my_projects.go index 93efc5c..ce10a7d 100644 --- a/telegram_bot/screens/my_projects.go +++ b/telegram_bot/screens/my_projects.go @@ -82,7 +82,6 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) var text string var buttons [][]echotron.InlineKeyboardButton - navTitle := "Главное меню › Мои проекты" headerTitle := "Мои проекты" emptyStateText := `У вас пока нет подключенных каналов. @@ -96,24 +95,18 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) Начните с добавления первого проекта` nonEmptyText := "Управление вашими Telegram-каналами.\n\nВыберите канал" if s.OpenPurchases { - navTitle = "Главное меню › Закупы" headerTitle = "Закупы" nonEmptyText = "Выберите проект для просмотра закупов" } if len(page.Items) == 0 { - text = fmt.Sprintf(`%s + text = fmt.Sprintf(`%s -%s - -%s`, navTitle, headerTitle, emptyStateText) +%s`, headerTitle, emptyStateText) } else { - text = fmt.Sprintf(`%s - -%s%s + text = fmt.Sprintf(`%s%s %s`, - navTitle, headerTitle, ui.FormatPageInfo(s.CurrentPage, page.Pages), nonEmptyText) diff --git a/telegram_bot/screens/project_details.go b/telegram_bot/screens/project_details.go index ea8c1bc..f6cb637 100644 --- a/telegram_bot/screens/project_details.go +++ b/telegram_bot/screens/project_details.go @@ -34,9 +34,7 @@ func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) { func (s *ProjectDetails) formatProjectDetails() string { p := s.Project - // Хлебные крошки и заголовок - text := fmt.Sprintf("… › Мои проекты › %s\n\n", p.Title) - text += fmt.Sprintf("%s\n\n", p.Title) + text := fmt.Sprintf("%s\n\n", p.Title) // Информация о канале if p.Username != nil && *p.Username != "" { @@ -132,10 +130,9 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { case strings.HasPrefix(data, "creatives:"): s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения b.SetState(&Creatives{ - ProjectID: s.Project.ID, - ProjectTitle: s.Project.Title, - WorkspaceID: s.WorkspaceID, - BackState: s, + ProjectID: s.Project.ID, + WorkspaceID: s.WorkspaceID, + BackState: s, }, bot.EditMessage) case strings.HasPrefix(data, "purchases:"): diff --git a/telegram_bot/screens/purchase_details.go b/telegram_bot/screens/purchase_details.go index f09abea..f5f648d 100644 --- a/telegram_bot/screens/purchase_details.go +++ b/telegram_bot/screens/purchase_details.go @@ -12,11 +12,10 @@ import ( ) type PurchaseDetails struct { - WorkspaceID string - ProjectID string - ProjectTitle string - PurchaseID string - BackState bot.State + WorkspaceID string + ProjectID string + PurchaseID string + BackState bot.State } func (s *PurchaseDetails) Enter(b *bot.Bot, mode bot.RenderMode) { @@ -63,7 +62,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str totalPlacements += len(items) } - text := "… › Закупы › Детали\n\nДетали закупа\n\n" + text := "Детали закупа\n\n" // Информация о креативе if creative != nil { diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go index 0e77f9c..bdb8136 100644 --- a/telegram_bot/screens/purchase_optional_details.go +++ b/telegram_bot/screens/purchase_optional_details.go @@ -70,8 +70,7 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) { return } - text := "… › Создание › Дополнительно\n\n" - text += "Страница создания закупа и необязательные составляющие" + text := "Страница создания закупа и необязательные составляющие" text += "\n\n" text += s.formatOptionalSummary() @@ -129,7 +128,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update) } case "opt_payment_date": - b.SetState(NewDateTimePicker(DateTimePickerConfig{ + b.SetState(ui.NewDateTimePicker(ui.DateTimePickerConfig{ Title: "Дата оплаты", Key: "payment_date", IncludeTime: true, @@ -380,8 +379,8 @@ 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")) + local := value.In(ui.MskLocation) + return fmt.Sprintf("%s %02d %s %s", ui.WeekdayName(local.Weekday()), local.Day(), ui.MonthShort(local.Month()), local.Format("15:04")) } func (s *PurchaseOptionalDetails) formatText(value string) string { @@ -500,7 +499,7 @@ func (s *PurchaseOptionalDetails) costBeforeDeleteRow() []echotron.InlineKeyboar } func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderMode) { - text := "… › Создание › Дополнительно\n\n" + text := "Дополнительно\n\n" switch s.InputMode { case "type": text += "Выберите тип закупа" @@ -598,8 +597,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM } func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMode) { - text := "… › Создание › Дополнительно › Выбор режима\n\n" - text += "⚙ Переключить режим параметра\n\n" + text := "⚙ Переключить режим параметра\n\n" // Показываем текущую информацию о параметрах text += s.formatOptionalSummary() @@ -671,8 +669,7 @@ func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.Render 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 := fmt.Sprintf("%s — по каналам\n\n", s.paramTitle(param)) text += s.renderParamChannelSummary(param) + "\n\n" const perPage = 5 @@ -866,7 +863,10 @@ func (s *PurchaseOptionalDetails) parseParamIndex(data, prefix string) (string, return "", 0, false } param := parts[0] - index := parseInt(parts[1]) + index, err := strconv.Atoi(parts[1]) + if err != nil { + return "", 0, false + } return param, index, true } @@ -875,7 +875,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) { case "placement": s.InputMode = "" s.ReturnMode = "" - b.SetState(NewDateTimePicker(DateTimePickerConfig{ + b.SetState(ui.NewDateTimePicker(ui.DateTimePickerConfig{ Title: "Дата и время размещения", Key: "placement_datetime", IncludeTime: true, @@ -907,7 +907,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, username switch param { case "placement": s.InputMode = "placement_channels" - b.SetState(NewDateTimePicker(DateTimePickerConfig{ + b.SetState(ui.NewDateTimePicker(ui.DateTimePickerConfig{ Title: "Дата и время размещения", Key: "placement_datetime:" + username, IncludeTime: true, @@ -968,7 +968,7 @@ func (s *PurchaseOptionalDetails) pasteParamValue(param, username string) { s.PlacementByChannel[username] = nil return } - value := s.PlacementCopy.In(mskLocation) + value := s.PlacementCopy.In(ui.MskLocation) s.PlacementByChannel[username] = &value case "cost": if s.CostCopy == nil { @@ -1467,11 +1467,10 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { } b.SetState(&PurchaseDetails{ - WorkspaceID: s.WorkspaceID, - ProjectID: s.ProjectID, - ProjectTitle: s.ProjectTitle, - PurchaseID: purchase.ID, - BackState: s.BackState, + WorkspaceID: s.WorkspaceID, + ProjectID: s.ProjectID, + PurchaseID: purchase.ID, + BackState: s.BackState, }, bot.NewMessage) } diff --git a/telegram_bot/screens/purchases.go b/telegram_bot/screens/purchases.go index fc76e9c..0b85c7c 100644 --- a/telegram_bot/screens/purchases.go +++ b/telegram_bot/screens/purchases.go @@ -40,12 +40,7 @@ func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string) return } - // Навигация: 2 последних уровня - navPath := "… › Закупы" - if s.ProjectTitle != "" { - navPath = fmt.Sprintf("… › %s › Закупы", s.ProjectTitle) - } - text := fmt.Sprintf("%s\n\nЗакупы\n\n", navPath) + text := "Закупы\n\n" var buttons [][]echotron.InlineKeyboardButton @@ -136,11 +131,10 @@ func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) { if len(data) > 9 && data[:9] == "purchase:" { purchaseID := data[9:] b.SetState(&PurchaseDetails{ - WorkspaceID: s.WorkspaceID, - ProjectID: s.ProjectID, - ProjectTitle: s.ProjectTitle, - PurchaseID: purchaseID, - BackState: s, + WorkspaceID: s.WorkspaceID, + ProjectID: s.ProjectID, + PurchaseID: purchaseID, + BackState: s, }, bot.EditMessage) } else { s.Enter(b, bot.NewMessage) diff --git a/telegram_bot/screens/select_channels_for_purchase.go b/telegram_bot/screens/select_channels_for_purchase.go index 689d893..84f408b 100644 --- a/telegram_bot/screens/select_channels_for_purchase.go +++ b/telegram_bot/screens/select_channels_for_purchase.go @@ -38,8 +38,7 @@ func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) { } func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.RenderMode) { - text := "… › Создание › Каналы\n\n" - text += "Создание закупа\n\n" + text := "Создание закупа\n\n" text += "Шаг 2/2: Добавление каналов\n\n" var buttons [][]echotron.InlineKeyboardButton @@ -268,11 +267,10 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) { } b.SetState(&PurchaseDetails{ - WorkspaceID: s.WorkspaceID, - ProjectID: s.ProjectID, - ProjectTitle: s.ProjectTitle, - PurchaseID: purchase.ID, - BackState: s.BackState, + WorkspaceID: s.WorkspaceID, + ProjectID: s.ProjectID, + PurchaseID: purchase.ID, + BackState: s.BackState, }, bot.EditMessage) } diff --git a/telegram_bot/screens/date_time_picker.go b/telegram_bot/ui/date_time_picker.go similarity index 77% rename from telegram_bot/screens/date_time_picker.go rename to telegram_bot/ui/date_time_picker.go index 7cd515e..7e8e7b8 100644 --- a/telegram_bot/screens/date_time_picker.go +++ b/telegram_bot/ui/date_time_picker.go @@ -1,4 +1,4 @@ -package screens +package ui import ( "fmt" @@ -18,7 +18,7 @@ const ( dtpCallbackPrefix = "dtp:" ) -var mskLocation = time.FixedZone("MSK", 3*60*60) +var MskLocation = time.FixedZone("MSK", 3*60*60) type DateTimePickerConfig struct { Title string @@ -47,13 +47,13 @@ type DateTimePicker struct { } func NewDateTimePicker(cfg DateTimePickerConfig) *DateTimePicker { - now := time.Now().In(mskLocation) + now := time.Now().In(MskLocation) year := now.Year() month := now.Month() var selected *time.Time if cfg.Selected != nil { - value := cfg.Selected.In(mskLocation) + value := cfg.Selected.In(MskLocation) selected = &value year = value.Year() month = value.Month() @@ -72,9 +72,8 @@ func NewDateTimePicker(cfg DateTimePickerConfig) *DateTimePicker { } func (s *DateTimePicker) Enter(b *bot.Bot, mode bot.RenderMode) { - text := fmt.Sprintf("… › %s\n\n", s.Title) - text += fmt.Sprintf("%s\n", s.formatSelectedDateTime()) - text += "\n" + text := fmt.Sprintf("%s\n\n", s.Title) + text += fmt.Sprintf("%s\n\n", s.formatSelectedDateTime()) switch s.view { case pickerViewCalendar: text += s.renderCalendarTitle() @@ -166,7 +165,7 @@ func (s *DateTimePicker) handleBack(b *bot.Bot) { } func (s *DateTimePicker) renderCalendarTitle() string { - if s.year == time.Now().In(mskLocation).Year() { + if s.year == time.Now().In(MskLocation).Year() { return "\n" } return fmt.Sprintf("%d\n\n", s.year) @@ -175,43 +174,46 @@ func (s *DateTimePicker) renderCalendarTitle() string { func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup { var rows [][]echotron.InlineKeyboardButton - rows = append(rows, Row( + rows = append(rows, []echotron.InlineKeyboardButton{ s.monthPrevButton(), - Button(monthName(s.month), dtpCallbackPrefix+"open_months"), - Button("→", dtpCallbackPrefix+"month_next"), - )) + {Text: monthName(s.month), CallbackData: dtpCallbackPrefix + "open_months"}, + {Text: "→", CallbackData: dtpCallbackPrefix + "month_next"}, + }) - rows = append(rows, Row( - Button("Пн", "empty"), - Button("Вт", "empty"), - Button("Ср", "empty"), - Button("Чт", "empty"), - Button("Пт", "empty"), - Button("Сб", "empty"), - Button("Вс", "empty"), - )) + rows = append(rows, []echotron.InlineKeyboardButton{ + {Text: "Пн", CallbackData: "empty"}, + {Text: "Вт", CallbackData: "empty"}, + {Text: "Ср", CallbackData: "empty"}, + {Text: "Чт", CallbackData: "empty"}, + {Text: "Пт", CallbackData: "empty"}, + {Text: "Сб", CallbackData: "empty"}, + {Text: "Вс", CallbackData: "empty"}, + }) - firstOfMonth := time.Date(s.year, s.month, 1, 0, 0, 0, 0, mskLocation) + firstOfMonth := time.Date(s.year, s.month, 1, 0, 0, 0, 0, MskLocation) weekday := int(firstOfMonth.Weekday()) if weekday == 0 { weekday = 7 } daysInMonth := daysInMonth(s.year, s.month) - today := time.Now().In(mskLocation) - todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, mskLocation) + today := time.Now().In(MskLocation) + todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, MskLocation) var row []echotron.InlineKeyboardButton for i := 1; i < weekday; i++ { - row = append(row, Button(" ", "empty")) + row = append(row, echotron.InlineKeyboardButton{Text: " ", CallbackData: "empty"}) } for day := 1; day <= daysInMonth; day++ { - date := time.Date(s.year, s.month, day, 0, 0, 0, 0, mskLocation) + date := time.Date(s.year, s.month, day, 0, 0, 0, 0, MskLocation) if date.Before(todayDate) { - row = append(row, Button("-", "empty")) + row = append(row, echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"}) } else { label := fmt.Sprintf("%d", day) - row = append(row, Button(label, fmt.Sprintf("%sday:%04d-%02d-%02d", dtpCallbackPrefix, s.year, int(s.month), day))) + row = append(row, echotron.InlineKeyboardButton{ + Text: label, + CallbackData: fmt.Sprintf("%sday:%04d-%02d-%02d", dtpCallbackPrefix, s.year, int(s.month), day), + }) } if len(row) == 7 { rows = append(rows, row) @@ -221,17 +223,17 @@ func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup { if len(row) > 0 { for len(row) < 7 { - row = append(row, Button(" ", "empty")) + row = append(row, echotron.InlineKeyboardButton{Text: " ", CallbackData: "empty"}) } rows = append(rows, row) } - rows = append(rows, Row( - Button("← Назад", "back"), - Button("Отмена", "cancel"), - )) + rows = append(rows, []echotron.InlineKeyboardButton{ + {Text: "← Назад", CallbackData: "back"}, + {Text: "Отмена", CallbackData: "cancel"}, + }) - return Keyboard(rows...) + return echotron.InlineKeyboardMarkup{InlineKeyboard: rows} } func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup { @@ -239,59 +241,61 @@ func (s *DateTimePicker) monthsKeyboard() echotron.InlineKeyboardMarkup { months := []string{"Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"} for i := 0; i < 12; i += 3 { - rows = append(rows, Row( - Button(months[i], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+1)), - Button(months[i+1], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+2)), - Button(months[i+2], fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+3)), - )) + rows = append(rows, []echotron.InlineKeyboardButton{ + {Text: months[i], CallbackData: fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+1)}, + {Text: months[i+1], CallbackData: fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+2)}, + {Text: months[i+2], CallbackData: fmt.Sprintf("%smonth:%d", dtpCallbackPrefix, i+3)}, + }) } - rows = append(rows, Row( - Button("← Назад", "back"), - )) + rows = append(rows, []echotron.InlineKeyboardButton{ + {Text: "← Назад", CallbackData: "back"}, + }) - return Keyboard(rows...) + return echotron.InlineKeyboardMarkup{InlineKeyboard: rows} } func (s *DateTimePicker) timeCombinedKeyboard() echotron.InlineKeyboardMarkup { var rows [][]echotron.InlineKeyboardButton - rows = append(rows, Row(Button("Часы", "empty"))) + rows = append(rows, []echotron.InlineKeyboardButton{{Text: "Часы", CallbackData: "empty"}}) for i := 0; i < 24; i += 6 { - rows = append(rows, Row( + rows = append(rows, []echotron.InlineKeyboardButton{ s.hourButton(i), - s.hourButton(i+1), - s.hourButton(i+2), - s.hourButton(i+3), - s.hourButton(i+4), - s.hourButton(i+5), - )) + s.hourButton(i + 1), + s.hourButton(i + 2), + s.hourButton(i + 3), + s.hourButton(i + 4), + s.hourButton(i + 5), + }) } - rows = append(rows, Row(Button("Минуты", "empty"))) + rows = append(rows, []echotron.InlineKeyboardButton{{Text: "Минуты", CallbackData: "empty"}}) minutes := []int{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55} for i := 0; i < len(minutes); i += 6 { - rows = append(rows, Row( + rows = append(rows, []echotron.InlineKeyboardButton{ s.minuteButton(minutes[i]), s.minuteButton(minutes[i+1]), s.minuteButton(minutes[i+2]), s.minuteButton(minutes[i+3]), s.minuteButton(minutes[i+4]), s.minuteButton(minutes[i+5]), - )) + }) } - rows = append(rows, Row( - Button("← Назад", "back"), - Button("Готово", dtpCallbackPrefix+"confirm"), - )) - return Keyboard(rows...) + rows = append(rows, []echotron.InlineKeyboardButton{ + {Text: "← Назад", CallbackData: "back"}, + {Text: "Готово", CallbackData: dtpCallbackPrefix + "confirm"}, + }) + return echotron.InlineKeyboardMarkup{InlineKeyboard: rows} } func (s *DateTimePicker) confirmKeyboard() echotron.InlineKeyboardMarkup { - return Keyboard( - Row( - Button("← Назад", "back"), - Button("Готово", dtpCallbackPrefix+"confirm"), - ), - ) + return echotron.InlineKeyboardMarkup{ + InlineKeyboard: [][]echotron.InlineKeyboardButton{ + { + {Text: "← Назад", CallbackData: "back"}, + {Text: "Готово", CallbackData: dtpCallbackPrefix + "confirm"}, + }, + }, + } } func (s *DateTimePicker) renderSelectedDateTime() string { @@ -306,7 +310,7 @@ func (s *DateTimePicker) renderSelectedDateTime() string { if s.selectedMinute != nil { minute = *s.selectedMinute } - value := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), hour, minute, 0, 0, mskLocation) + value := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), hour, minute, 0, 0, MskLocation) return fmt.Sprintf("%s", formatCompactDateTime(value)) } @@ -335,15 +339,15 @@ func (s *DateTimePicker) formatSelectedDateTime() string { if s.selectedHour != nil && s.selectedMinute != nil { timeLabel = fmt.Sprintf("%02d:%02d", *s.selectedHour, *s.selectedMinute) } - weekday := weekdayName(displayDate.Weekday()) - month := monthShort(displayDate.Month()) + weekday := WeekdayName(displayDate.Weekday()) + month := MonthShort(displayDate.Month()) return fmt.Sprintf("%s %02d %s %s %dг.", weekday, displayDate.Day(), month, timeLabel, displayDate.Year()) } func (s *DateTimePicker) displayDate() (time.Time, bool) { if s.selectedDate == nil { if s.view == pickerViewCalendar || s.view == pickerViewMonths { - return time.Date(s.year, s.month, 1, 0, 0, 0, 0, mskLocation), true + return time.Date(s.year, s.month, 1, 0, 0, 0, 0, MskLocation), true } return time.Time{}, false } @@ -355,15 +359,15 @@ func (s *DateTimePicker) displayDate() (time.Time, bool) { if day > maxDay { day = maxDay } - return time.Date(s.year, s.month, day, 0, 0, 0, 0, mskLocation), true + return time.Date(s.year, s.month, day, 0, 0, 0, 0, MskLocation), true } - return s.selectedDate.In(mskLocation), true + return s.selectedDate.In(MskLocation), true } func (s *DateTimePicker) handleDaySelect(payload string) { datePart := strings.TrimPrefix(payload, "day:") - parsed, err := time.ParseInLocation("2006-01-02", datePart, mskLocation) + parsed, err := time.ParseInLocation("2006-01-02", datePart, MskLocation) if err != nil { return } @@ -401,9 +405,12 @@ func (s *DateTimePicker) handleMinuteSelect(payload string) { func (s *DateTimePicker) confirmSelection(b *bot.Bot) { if s.selectedDate == nil { + s.Enter(b, bot.EditMessage) return } if s.IncludeTime && (s.selectedHour == nil || s.selectedMinute == nil) { + s.view = pickerViewTimeCombined + s.Enter(b, bot.EditMessage) return } hour := 0 @@ -414,7 +421,7 @@ func (s *DateTimePicker) confirmSelection(b *bot.Bot) { if s.selectedMinute != nil { minute = *s.selectedMinute } - value := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), hour, minute, 0, 0, mskLocation) + value := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), hour, minute, 0, 0, MskLocation) if target, ok := s.BackState.(DateTimeSelectionTarget); ok { target.SetDateTimeSelection(s.Key, value) } @@ -426,32 +433,38 @@ func (s *DateTimePicker) confirmSelection(b *bot.Bot) { func (s *DateTimePicker) hourButton(hour int) echotron.InlineKeyboardButton { label := fmt.Sprintf("%02d", hour) if s.isHourDisabled(hour) { - return Button("-", "empty") + return echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"} } if s.selectedHour != nil && *s.selectedHour == hour { label = "● " + label } - return Button(label, fmt.Sprintf("%shour:%02d", dtpCallbackPrefix, hour)) + return echotron.InlineKeyboardButton{ + Text: label, + CallbackData: fmt.Sprintf("%shour:%02d", dtpCallbackPrefix, hour), + } } func (s *DateTimePicker) minuteButton(minute int) echotron.InlineKeyboardButton { label := fmt.Sprintf("%02d", minute) if s.isMinuteDisabled(minute) { - return Button("-", "empty") + return echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"} } if s.selectedMinute != nil && *s.selectedMinute == minute { label = "● " + label } - return Button(label, fmt.Sprintf("%smin:%02d", dtpCallbackPrefix, minute)) + return echotron.InlineKeyboardButton{ + Text: label, + CallbackData: fmt.Sprintf("%smin:%02d", dtpCallbackPrefix, minute), + } } func (s *DateTimePicker) isHourDisabled(hour int) bool { if s.selectedDate == nil { return false } - today := time.Now().In(mskLocation) - date := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), 0, 0, 0, 0, mskLocation) - todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, mskLocation) + today := time.Now().In(MskLocation) + date := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), 0, 0, 0, 0, MskLocation) + todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, MskLocation) if date.After(todayDate) { return false } @@ -462,9 +475,9 @@ func (s *DateTimePicker) isMinuteDisabled(minute int) bool { if s.selectedDate == nil || s.selectedHour == nil { return false } - today := time.Now().In(mskLocation) - date := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), 0, 0, 0, 0, mskLocation) - todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, mskLocation) + today := time.Now().In(MskLocation) + date := time.Date(s.selectedDate.Year(), s.selectedDate.Month(), s.selectedDate.Day(), 0, 0, 0, 0, MskLocation) + todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, MskLocation) if date.After(todayDate) { return false } @@ -475,7 +488,7 @@ func (s *DateTimePicker) isMinuteDisabled(minute int) bool { } func (s *DateTimePicker) prevMonth() { - now := time.Now().In(mskLocation) + now := time.Now().In(MskLocation) if s.year == now.Year() && s.month == now.Month() { return } @@ -497,15 +510,15 @@ func (s *DateTimePicker) nextMonth() { } func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton { - now := time.Now().In(mskLocation) + now := time.Now().In(MskLocation) if s.year == now.Year() && s.month == now.Month() { - return Button("-", "empty") + return echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"} } - return Button("←", dtpCallbackPrefix+"month_prev") + return echotron.InlineKeyboardButton{Text: "←", CallbackData: dtpCallbackPrefix + "month_prev"} } func daysInMonth(year int, month time.Month) int { - return time.Date(year, month+1, 0, 0, 0, 0, 0, mskLocation).Day() + return time.Date(year, month+1, 0, 0, 0, 0, 0, MskLocation).Day() } func monthName(month time.Month) string { @@ -598,10 +611,10 @@ func parseDateTimeInput( text = strings.ReplaceAll(text, ",", " ") text = strings.ReplaceAll(text, " ", " ") - now := time.Now().In(mskLocation) + now := time.Now().In(MskLocation) baseDate := now if currentDate != nil { - baseDate = currentDate.In(mskLocation) + baseDate = currentDate.In(MskLocation) } date, dateOk := parseDate(text, baseDate) @@ -611,14 +624,14 @@ func parseDateTimeInput( return time.Time{}, false, false } if !dateOk { - date = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), 0, 0, 0, 0, mskLocation) + date = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), 0, 0, 0, 0, MskLocation) } if !timeOk { hour = 0 minute = 0 } - value := time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, mskLocation) + value := time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, MskLocation) if value.Before(now) { return time.Time{}, false, false } @@ -658,7 +671,7 @@ func parseNumericDate(text string) time.Time { if match := reDMY.FindStringSubmatch(text); len(match) >= 3 { day := parseInt(match[1]) month := parseInt(match[2]) - year := time.Now().In(mskLocation).Year() + year := time.Now().In(MskLocation).Year() if len(match) == 4 && match[3] != "" { year = parseInt(match[3]) if year < 100 { @@ -705,7 +718,7 @@ func parseWordDate(text string) time.Time { return time.Time{} } - year := time.Now().In(mskLocation).Year() + year := time.Now().In(MskLocation).Year() if len(match) == 4 && match[3] != "" { year = parseInt(match[3]) if year < 100 { @@ -772,10 +785,10 @@ func safeDate(year, month, day int) time.Time { if day < 1 || day > 31 { return time.Time{} } - return time.Date(year, time.Month(month), day, 0, 0, 0, 0, mskLocation) + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, MskLocation) } -func weekdayName(day time.Weekday) string { +func WeekdayName(day time.Weekday) string { switch day { case time.Monday: return "Пн" @@ -797,12 +810,12 @@ func weekdayName(day time.Weekday) string { } func formatCompactDateTime(value time.Time) string { - weekday := weekdayName(value.Weekday()) - month := monthShort(value.Month()) + weekday := WeekdayName(value.Weekday()) + month := MonthShort(value.Month()) return fmt.Sprintf("%s %02d %s %s", weekday, value.Day(), month, value.Format("15:04")) } -func monthShort(month time.Month) string { +func MonthShort(month time.Month) string { months := []string{ "янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек",