change domen
This commit is contained in:
@@ -141,12 +141,12 @@ type Workspace struct {
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
TelegramID int64 `json:"telegram_id"`
|
||||
Title string `json:"title"`
|
||||
Username *string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
InviteLinkType string `json:"invite_link_type"` // "public" или "approval"
|
||||
ID string `json:"id"`
|
||||
TelegramID int64 `json:"telegram_id"`
|
||||
Title string `json:"title"`
|
||||
Username *string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
PurchaseInviteTypeDefault string `json:"purchase_invite_type_default"`
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
@@ -231,7 +231,7 @@ func (c *Client) UpdateProjectInviteLinkType(
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/invite-link-type", workspaceID, projectID)
|
||||
|
||||
payload := map[string]string{
|
||||
"invite_link_type": inviteLinkType,
|
||||
"purchase_invite_type_default": inviteLinkType,
|
||||
}
|
||||
|
||||
var project Project
|
||||
@@ -306,13 +306,14 @@ func (c *Client) AcceptWorkspaceInvite(
|
||||
// ============================================================================
|
||||
|
||||
type Creative struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"text"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"text"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ProjectChannelTitle string `json:"project_channel_title"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
PlacementsCount int `json:"placements_count"`
|
||||
}
|
||||
|
||||
type CreativesPage struct {
|
||||
@@ -406,9 +407,9 @@ func (c *Client) CreateCreative(
|
||||
}
|
||||
|
||||
type UpdateCreativeInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
IsArchived *bool `json:"is_archived,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) UpdateCreative(
|
||||
@@ -456,17 +457,24 @@ func (c *Client) DeleteCreative(
|
||||
// ============================================================================
|
||||
|
||||
type Placement struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
PlacementChannelID string `json:"placement_channel_id"`
|
||||
CreativeID *string `json:"creative_id"`
|
||||
Name string `json:"name"`
|
||||
Price *int `json:"price"`
|
||||
Status string `json:"status"`
|
||||
InviteLink *string `json:"invite_link"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ProjectChannelTitle *string `json:"project_channel_title"`
|
||||
PlacementChannelID string `json:"placement_channel_id"`
|
||||
PlacementChannelTitle *string `json:"placement_channel_title"`
|
||||
CreativeID string `json:"creative_id"`
|
||||
CreativeName string `json:"creative_name"`
|
||||
PlacementDate string `json:"placement_date"`
|
||||
Cost *float64 `json:"cost"`
|
||||
Comment *string `json:"comment"`
|
||||
AdPostURL *string `json:"ad_post_url"`
|
||||
InviteLinkType string `json:"invite_link_type"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
Status string `json:"status"`
|
||||
SubscriptionsCount int `json:"subscriptions_count"`
|
||||
PurchaseID *string `json:"purchase_id"`
|
||||
PurchaseChannelID *string `json:"purchase_channel_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type PlacementsPage struct {
|
||||
@@ -537,84 +545,10 @@ func (c *Client) GetPlacement(
|
||||
return &placement, err
|
||||
}
|
||||
|
||||
type CreatePlacementInput struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
PlacementChannelID string `json:"placement_channel_id"`
|
||||
CreativeID *string `json:"creative_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Price *int `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) CreatePlacement(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
input CreatePlacementInput,
|
||||
) (*Placement, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/placements", workspaceID)
|
||||
|
||||
var placement Placement
|
||||
err := c.do(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
path,
|
||||
input,
|
||||
&placement,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &placement, err
|
||||
}
|
||||
|
||||
type UpdatePlacementInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Price *int `json:"price,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
IsArchived *bool `json:"is_archived,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) UpdatePlacement(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
placementID string,
|
||||
input UpdatePlacementInput,
|
||||
) (*Placement, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/placements/%s", workspaceID, placementID)
|
||||
|
||||
var placement Placement
|
||||
err := c.do(
|
||||
ctx,
|
||||
http.MethodPatch,
|
||||
path,
|
||||
input,
|
||||
&placement,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &placement, err
|
||||
}
|
||||
|
||||
func (c *Client) DeletePlacement(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
placementID string,
|
||||
) error {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/placements/%s", workspaceID, placementID)
|
||||
|
||||
return c.do(
|
||||
ctx,
|
||||
http.MethodDelete,
|
||||
path,
|
||||
nil,
|
||||
nil,
|
||||
withBearer(jwt),
|
||||
)
|
||||
}
|
||||
// Note: Placement creation/updates are handled by worker; API exposes only read endpoints.
|
||||
|
||||
// ============================================================================
|
||||
// Purchase Plan
|
||||
// Purchases (Закупы)
|
||||
// ============================================================================
|
||||
|
||||
type Channel struct {
|
||||
@@ -624,24 +558,67 @@ type Channel struct {
|
||||
Username *string `json:"username"`
|
||||
}
|
||||
|
||||
type PurchasePlanChannel struct {
|
||||
ID string `json:"id"`
|
||||
Channel Channel `json:"channel"`
|
||||
Status string `json:"status"`
|
||||
PlannedCost *float64 `json:"planned_cost"`
|
||||
Comment *string `json:"comment"`
|
||||
type Purchase struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreativeID string `json:"creative_id"`
|
||||
Channels []PurchaseChannelOut `json:"channels"`
|
||||
}
|
||||
|
||||
func (c *Client) GetPurchasePlanChannels(
|
||||
type PurchaseChannelOut struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
PlannedCost *float64 `json:"planned_cost"`
|
||||
Comment *string `json:"comment"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
InviteLinkType string `json:"invite_link_type"`
|
||||
Channel Channel `json:"channel"`
|
||||
}
|
||||
|
||||
type CreatePurchaseChannelInput struct {
|
||||
Username string `json:"username"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
PlannedCost *float64 `json:"planned_cost,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type CreatePurchaseInput struct {
|
||||
CreativeID string `json:"creative_id"`
|
||||
Channels []CreatePurchaseChannelInput `json:"channels"`
|
||||
}
|
||||
|
||||
func (c *Client) CreatePurchase(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
projectID string,
|
||||
) ([]PurchasePlanChannel, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels", workspaceID, projectID)
|
||||
input CreatePurchaseInput,
|
||||
) (*Purchase, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase", workspaceID, projectID)
|
||||
|
||||
var purchase Purchase
|
||||
err := c.do(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
path,
|
||||
input,
|
||||
&purchase,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &purchase, err
|
||||
}
|
||||
|
||||
func (c *Client) GetPurchases(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
projectID string,
|
||||
) ([]Purchase, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase", workspaceID, projectID)
|
||||
|
||||
var resp struct {
|
||||
Items []PurchasePlanChannel `json:"items"`
|
||||
Items []Purchase `json:"items"`
|
||||
}
|
||||
|
||||
err := c.do(
|
||||
@@ -656,81 +633,26 @@ func (c *Client) GetPurchasePlanChannels(
|
||||
return resp.Items, err
|
||||
}
|
||||
|
||||
type AttachChannelToPurchasePlanInput struct {
|
||||
Username string `json:"username"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
PlannedCost *float64 `json:"planned_cost,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) AttachChannelToPurchasePlan(
|
||||
func (c *Client) GetPurchase(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
projectID string,
|
||||
input AttachChannelToPurchasePlanInput,
|
||||
) (*PurchasePlanChannel, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels", workspaceID, projectID)
|
||||
purchaseID string,
|
||||
) (*Purchase, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase/%s", workspaceID, projectID, purchaseID)
|
||||
|
||||
var planChannel PurchasePlanChannel
|
||||
var purchase Purchase
|
||||
err := c.do(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
path,
|
||||
input,
|
||||
&planChannel,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &planChannel, err
|
||||
}
|
||||
|
||||
type UpdatePurchasePlanChannelInput struct {
|
||||
Status *string `json:"status,omitempty"`
|
||||
PlannedCost *float64 `json:"planned_cost,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) UpdatePurchasePlanChannel(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
projectID string,
|
||||
channelID string,
|
||||
input UpdatePurchasePlanChannelInput,
|
||||
) (*PurchasePlanChannel, error) {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels/%s", workspaceID, projectID, channelID)
|
||||
|
||||
var planChannel PurchasePlanChannel
|
||||
err := c.do(
|
||||
ctx,
|
||||
http.MethodPatch,
|
||||
path,
|
||||
input,
|
||||
&planChannel,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &planChannel, err
|
||||
}
|
||||
|
||||
func (c *Client) RemoveChannelFromPurchasePlan(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
projectID string,
|
||||
channelID string,
|
||||
) error {
|
||||
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels/%s", workspaceID, projectID, channelID)
|
||||
|
||||
return c.do(
|
||||
ctx,
|
||||
http.MethodDelete,
|
||||
http.MethodGet,
|
||||
path,
|
||||
nil,
|
||||
nil,
|
||||
&purchase,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &purchase, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -101,6 +101,7 @@ func (b *Bot) Render(text string, keyboard echotron.InlineKeyboardMarkup, mode R
|
||||
func (b *Bot) SendNew(text string, keyboard echotron.InlineKeyboardMarkup) {
|
||||
// Удаляем кнопки у предыдущего сообщения перед отправкой нового
|
||||
if b.LastMessageID != 0 {
|
||||
log.Info().Int("cleanup_msg_id", b.LastMessageID).Msg("Cleaning up keyboard before SendNew")
|
||||
b.cleanupKeyboard(b.LastMessageID)
|
||||
}
|
||||
|
||||
@@ -120,6 +121,7 @@ func (b *Bot) SendNew(text string, keyboard echotron.InlineKeyboardMarkup) {
|
||||
|
||||
if res.Result != nil {
|
||||
b.LastMessageID = res.Result.ID
|
||||
log.Info().Int("new_last_msg_id", b.LastMessageID).Msg("Updated LastMessageID in SendNew")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +280,7 @@ func (b *Bot) cleanupCallbackUI(u *echotron.Update) {
|
||||
if u.CallbackQuery.Message != nil {
|
||||
callbackMessageID := u.CallbackQuery.Message.ID
|
||||
|
||||
// Если это сообщение последнее, не удаляем клавиатуру
|
||||
if b.LastMessageID == callbackMessageID {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"example.com/m/adapter/backend"
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
type AddPlacement struct {
|
||||
WorkspaceID string
|
||||
BackState bot.State
|
||||
|
||||
// Выбранные данные
|
||||
SelectedProject *backend.Project
|
||||
SelectedCreative *backend.Creative
|
||||
}
|
||||
|
||||
func (s *AddPlacement) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
text := `<b>➕ Создать закуп</b>
|
||||
|
||||
Шаг 1: Выберите проект и креатив
|
||||
|
||||
Нажмите на кнопки ниже для выбора:`
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
// Кнопка выбора проекта
|
||||
projectButtonText := "Проект: Не выбран"
|
||||
if s.SelectedProject != nil {
|
||||
projectButtonText = fmt.Sprintf("Проект: %s", s.SelectedProject.Title)
|
||||
}
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(projectButtonText, "select_project"),
|
||||
))
|
||||
|
||||
// Кнопка выбора креатива
|
||||
creativeButtonText := "Креатив: Не выбран"
|
||||
if s.SelectedCreative != nil {
|
||||
creativeButtonText = fmt.Sprintf("Креатив: %s", s.SelectedCreative.Name)
|
||||
}
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(creativeButtonText, "select_creative"),
|
||||
))
|
||||
|
||||
// Кнопки навигации
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
navRow = append(navRow, bot.Button("« Назад", "cancel"))
|
||||
|
||||
// Кнопка "Далее" активна только если выбран проект
|
||||
if s.SelectedProject != nil {
|
||||
navRow = append(navRow, bot.Button("Далее →", "next"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, navRow)
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *AddPlacement) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch data {
|
||||
case "cancel":
|
||||
if s.BackState != nil {
|
||||
b.SetState(s.BackState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case "select_project":
|
||||
// Переходим к выбору проекта
|
||||
b.SetState(&SelectProjectForPlacement{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
AddPlacementState: s,
|
||||
}, bot.NewMessage)
|
||||
|
||||
case "select_creative":
|
||||
if s.SelectedProject == nil {
|
||||
// Если проект не выбран, показываем предупреждение
|
||||
b.Edit(`<b>⚠️ Сначала выберите проект</b>
|
||||
|
||||
Для выбора креатива необходимо сначала выбрать проект, так как креативы привязаны к проектам.`, bot.Keyboard(
|
||||
bot.Row(bot.Button("« Назад", "back_to_add_placement")),
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// Переходим к выбору креатива
|
||||
b.SetState(&SelectCreativeForPlacement{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.SelectedProject.ID,
|
||||
AddPlacementState: s,
|
||||
}, bot.NewMessage)
|
||||
|
||||
case "back_to_add_placement":
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case "next":
|
||||
if s.SelectedProject == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Переходим к управлению планом закупа (добавление/удаление каналов)
|
||||
b.SetState(&PlacementPurchasePlan{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.SelectedProject.ID,
|
||||
AddPlacementState: s,
|
||||
}, bot.EditMessage)
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AddPlacement) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *AddPlacement) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
160
telegram_bot/screens/add_purchase.go
Normal file
160
telegram_bot/screens/add_purchase.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const addPurchaseCreativesPerPage = 5
|
||||
|
||||
type AddPurchase struct {
|
||||
WorkspaceID string
|
||||
ProjectID string
|
||||
CurrentPage int
|
||||
BackState bot.State
|
||||
}
|
||||
|
||||
func (s *AddPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderCreativeSelection(b, mode, jwt)
|
||||
}
|
||||
|
||||
func (s *AddPurchase) renderCreativeSelection(b *bot.Bot, mode bot.RenderMode, jwt string) {
|
||||
// Получаем список активных креативов
|
||||
page, err := b.Backend.GetCreatives(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
&s.ProjectID,
|
||||
false, // только активные
|
||||
s.CurrentPage+1,
|
||||
addPurchaseCreativesPerPage,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get creatives")
|
||||
b.SendNew("❌ Не удалось загрузить креативы", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
text := "<b>➕ Создание закупа</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(page.Items) == 0 {
|
||||
text += `Для создания закупа сначала нужно создать креатив.
|
||||
|
||||
<i>Вернитесь назад и создайте креатив в разделе "Креативы"</i>`
|
||||
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("← Назад", "back"),
|
||||
))
|
||||
} else {
|
||||
pageInfo := ""
|
||||
if page.Pages > 1 {
|
||||
pageInfo = fmt.Sprintf(" (стр. %d/%d)", page.Page, page.Pages)
|
||||
}
|
||||
|
||||
text += fmt.Sprintf(`<b>Шаг 1/2: Выбор креатива</b>%s
|
||||
|
||||
<i>Выберите креатив для закупа:</i>
|
||||
|
||||
`, pageInfo)
|
||||
|
||||
// Кнопки с креативами
|
||||
for _, creative := range page.Items {
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)),
|
||||
))
|
||||
}
|
||||
|
||||
// Пагинация
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
if s.CurrentPage > 0 {
|
||||
navRow = append(navRow, bot.Button("←", "prev"))
|
||||
}
|
||||
if s.CurrentPage+1 < page.Pages {
|
||||
navRow = append(navRow, bot.Button("→", "next"))
|
||||
}
|
||||
if len(navRow) > 0 {
|
||||
buttons = append(buttons, navRow)
|
||||
}
|
||||
|
||||
// Кнопка назад
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("← Отмена", "back"),
|
||||
))
|
||||
}
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *AddPurchase) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch data {
|
||||
case "back":
|
||||
if s.BackState != nil {
|
||||
b.SetState(s.BackState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case "prev":
|
||||
if s.CurrentPage > 0 {
|
||||
s.CurrentPage--
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case "next":
|
||||
s.CurrentPage++
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
default:
|
||||
// Проверяем, не выбрали ли креатив
|
||||
if len(data) > 9 && data[:9] == "creative:" {
|
||||
creativeID := data[9:]
|
||||
|
||||
// Переходим к выбору каналов
|
||||
b.SetState(&SelectChannelsForPurchase{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.ProjectID,
|
||||
CreativeID: creativeID,
|
||||
Channels: []PurchaseChannelInput{},
|
||||
BackState: s.BackState,
|
||||
}, bot.EditMessage)
|
||||
} else {
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AddPurchase) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *AddPurchase) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
@@ -46,9 +46,19 @@ func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
s.Name = &creative.Name
|
||||
if creative.Text != "" {
|
||||
s.Text = &creative.Text
|
||||
log.Info().Str("creative_text", creative.Text).Msg("Set creative text from backend")
|
||||
} else {
|
||||
log.Info().Msg("Creative text is empty from backend")
|
||||
}
|
||||
// Buttons, MediaType, MediaFileID остаются пустыми (не сохраняются в БД)
|
||||
|
||||
log.Info().
|
||||
Str("creative_id", s.CreativeID).
|
||||
Str("name", creative.Name).
|
||||
Bool("has_text", creative.Text != "").
|
||||
Bool("s_text_is_nil", s.Text == nil).
|
||||
Msg("Sending creative preview and panel")
|
||||
|
||||
// Отправляем превью и панель управления
|
||||
s.SendCreativePreview(b)
|
||||
s.showManagementPanelWithDelete(b)
|
||||
|
||||
@@ -2,6 +2,7 @@ package screens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
@@ -43,8 +44,24 @@ type CreativeEditorFields struct {
|
||||
// GetCreativeText возвращает текст для превью креатива
|
||||
func (e *CreativeEditorFields) GetCreativeText() string {
|
||||
if e.Text != nil && *e.Text != "" {
|
||||
// Проверяем что текст не является "визуально пустым" HTML
|
||||
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
|
||||
text := strings.TrimSpace(*e.Text)
|
||||
|
||||
// Убираем пустые HTML-теги (простая проверка)
|
||||
if text == "<a class=\"tg-link\"></a>" || text == "" {
|
||||
log.Info().Str("text", *e.Text).Msg("GetCreativeText: text is visually empty, using default")
|
||||
return `<i>💭 Я твой креатив
|
||||
|
||||
У меня пока нет текста, добавь его ниже.
|
||||
|
||||
</i>`
|
||||
}
|
||||
|
||||
log.Info().Str("text", *e.Text).Msg("GetCreativeText: returning creative text")
|
||||
return *e.Text
|
||||
}
|
||||
log.Info().Bool("text_is_nil", e.Text == nil).Msg("GetCreativeText: returning default text")
|
||||
return `<i>💭 Я твой креатив
|
||||
|
||||
У меня пока нет текста, добавь его ниже.
|
||||
@@ -136,7 +153,7 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
|
||||
}
|
||||
} else {
|
||||
// Отправляем обычное текстовое сообщение напрямую (не через SendNew)
|
||||
// чтобы не обновлять LastMessageID и не затронуть cleanup
|
||||
// чтобы не затронуть LastMessageID и не затронуть cleanup
|
||||
res, err := b.SendMessage(creativeText, b.ChatID, &echotron.MessageOptions{
|
||||
ReplyMarkup: keyboard,
|
||||
ParseMode: echotron.HTML,
|
||||
@@ -319,12 +336,15 @@ func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons
|
||||
} else {
|
||||
// Редактируем существующую панель
|
||||
log.Info().Int("control_panel_msg_id", *e.ControlPanelMessageID).Msg("Editing control panel")
|
||||
|
||||
// Временно заменяем LastMessageID на ID панели управления
|
||||
oldLastMessageID := b.LastMessageID
|
||||
b.LastMessageID = *e.ControlPanelMessageID
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Edit(text, keyboard)
|
||||
|
||||
// Восстанавливаем оригинальный LastMessageID
|
||||
b.LastMessageID = oldLastMessageID
|
||||
log.Info().Msg("Control panel updated")
|
||||
}
|
||||
|
||||
@@ -104,24 +104,20 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string)
|
||||
buttons = append(buttons, row)
|
||||
}
|
||||
|
||||
// Ряд: Пагинация + Добавить
|
||||
// Ряд: Пагинация + Добавить (динамическая ширина)
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
|
||||
// Левая стрелка (или пустая кнопка)
|
||||
// Левая стрелка (только если есть предыдущая страница)
|
||||
if s.CurrentPage > 0 {
|
||||
navRow = append(navRow, bot.Button("←", "prev"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button(" ", "empty"))
|
||||
}
|
||||
|
||||
// Кнопка "Добавить креатив" всегда в центре
|
||||
navRow = append(navRow, bot.Button("+ Добавить", "add_creative"))
|
||||
|
||||
// Правая стрелка (или пустая кнопка)
|
||||
// Правая стрелка (только если есть следующая страница)
|
||||
if s.CurrentPage+1 < page.Pages {
|
||||
navRow = append(navRow, bot.Button("→", "next"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button(" ", "empty"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, navRow)
|
||||
|
||||
@@ -19,11 +19,11 @@ func (s *MainMenu) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
func (s *MainMenu) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
switch u.CallbackQuery.Data {
|
||||
case "my_projects":
|
||||
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.EditMessage)
|
||||
b.SetState(&MyProjects{BackState: &MainMenu{}}, bot.NewMessage)
|
||||
case "support":
|
||||
// b.SetState(&Support{}, bot.NewMessage)
|
||||
case "placements":
|
||||
b.SetState(&MyPlacements{BackState: &MainMenu{}}, bot.EditMessage)
|
||||
b.SetState(&MyPlacements{BackState: &MainMenu{}}, bot.NewMessage)
|
||||
case "dashboard":
|
||||
// b.SetState(&Dashboard{}, bot.NewMessage)
|
||||
default:
|
||||
|
||||
@@ -66,27 +66,37 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
|
||||
return
|
||||
}
|
||||
|
||||
text := "Мои закупы • Управление размещениями\n\n"
|
||||
text := "<b>📍 Мои размещения</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(page.Items) == 0 {
|
||||
text += "У вас пока нет закупов"
|
||||
} else {
|
||||
text += fmt.Sprintf("Выберите закуп для управления (стр. %d из %d):", s.CurrentPage+1, page.Pages)
|
||||
text += `У вас пока нет размещений.
|
||||
|
||||
// Закупы по 2 в ряду
|
||||
<i>Размещения создаются автоматически при создании закупа.</i>`
|
||||
} else {
|
||||
text += fmt.Sprintf("Выберите размещение для деталей (стр. %d из %d):\n\n", s.CurrentPage+1, page.Pages)
|
||||
|
||||
// Размещения по 2 в ряду
|
||||
for i := 0; i < len(page.Items); i += 2 {
|
||||
var row []echotron.InlineKeyboardButton
|
||||
|
||||
// Первый закуп в ряду
|
||||
// Первый placement в ряду
|
||||
placement := page.Items[i]
|
||||
row = append(row, bot.Button(placement.Name, fmt.Sprintf("placement:%s", placement.ID)))
|
||||
placementName := placement.CreativeName
|
||||
if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" {
|
||||
placementName = fmt.Sprintf("%s → %s", placement.CreativeName, *placement.PlacementChannelTitle)
|
||||
}
|
||||
row = append(row, bot.Button(placementName, fmt.Sprintf("placement:%s", placement.ID)))
|
||||
|
||||
// Второй закуп в ряду (если есть)
|
||||
// Второй placement в ряду (если есть)
|
||||
if i+1 < len(page.Items) {
|
||||
placement = page.Items[i+1]
|
||||
row = append(row, bot.Button(placement.Name, fmt.Sprintf("placement:%s", placement.ID)))
|
||||
placementName = placement.CreativeName
|
||||
if placement.PlacementChannelTitle != nil && *placement.PlacementChannelTitle != "" {
|
||||
placementName = fmt.Sprintf("%s → %s", placement.CreativeName, *placement.PlacementChannelTitle)
|
||||
}
|
||||
row = append(row, bot.Button(placementName, fmt.Sprintf("placement:%s", placement.ID)))
|
||||
} else {
|
||||
// Добавляем пустую кнопку для выравнивания
|
||||
row = append(row, bot.Button(" ", "empty"))
|
||||
@@ -121,12 +131,11 @@ func (s *MyPlacements) renderPlacements(b *bot.Bot, mode bot.RenderMode, jwt str
|
||||
|
||||
// Кнопки управления
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("Статистика", "statistics"),
|
||||
bot.Button("Создать закуп", "add_placement"),
|
||||
bot.Button("📊 Статистика", "statistics"),
|
||||
))
|
||||
|
||||
// Кнопка возврата в главное меню
|
||||
buttons = append(buttons, bot.Row(bot.Button("🏠 Главное меню", "menu")))
|
||||
buttons = append(buttons, bot.Row(bot.Button("← Назад", "back")))
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
@@ -167,8 +176,12 @@ func (s *MyPlacements) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
s.CurrentPage++
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case data == "menu":
|
||||
b.SetState(&MainMenu{}, bot.EditMessage)
|
||||
case data == "back":
|
||||
if s.BackState != nil {
|
||||
b.SetState(s.BackState, bot.EditMessage)
|
||||
} else {
|
||||
b.SetState(&MainMenu{}, bot.EditMessage)
|
||||
}
|
||||
|
||||
case data == "page_info", data == "empty":
|
||||
// Неактивные кнопки - ничего не делаем
|
||||
@@ -176,16 +189,10 @@ func (s *MyPlacements) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
|
||||
case data == "statistics":
|
||||
// TODO: реализовать экран статистики
|
||||
b.Edit("📊 Статистика закупов\n\nЭта функция в разработке", bot.Keyboard(
|
||||
b.Edit("📊 Статистика размещений\n\nЭта функция в разработке", bot.Keyboard(
|
||||
bot.Row(bot.Button("« Назад", "back_to_placements")),
|
||||
))
|
||||
|
||||
case data == "add_placement":
|
||||
b.SetState(&AddPlacement{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
BackState: s,
|
||||
}, bot.EditMessage)
|
||||
|
||||
case data == "back_to_placements":
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
|
||||
@@ -115,24 +115,20 @@ func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string)
|
||||
buttons = append(buttons, row)
|
||||
}
|
||||
|
||||
// Ряд: Пагинация + Добавить
|
||||
// Ряд: Пагинация + Добавить (динамическая ширина)
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
|
||||
// Левая стрелка (или пустая кнопка)
|
||||
// Левая стрелка (только если есть предыдущая страница)
|
||||
if s.CurrentPage > 0 {
|
||||
navRow = append(navRow, bot.Button("←", "prev"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button(" ", "empty"))
|
||||
}
|
||||
|
||||
// Кнопка "Добавить проект" всегда в центре
|
||||
navRow = append(navRow, bot.Button("+ Добавить", "add_project"))
|
||||
|
||||
// Правая стрелка (или пустая кнопка)
|
||||
// Правая стрелка (только если есть следующая страница)
|
||||
if s.CurrentPage+1 < page.Pages {
|
||||
navRow = append(navRow, bot.Button("→", "next"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button(" ", "empty"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, navRow)
|
||||
@@ -224,7 +220,7 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
Project: project,
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
BackState: &MyProjects{WorkspaceID: s.WorkspaceID},
|
||||
}, bot.EditMessage)
|
||||
}, bot.NewMessage)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(data, "workspace:"):
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"example.com/m/adapter/backend"
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PlacementPurchasePlan struct {
|
||||
WorkspaceID string
|
||||
ProjectID string
|
||||
AddPlacementState *AddPlacement
|
||||
}
|
||||
|
||||
func (s *PlacementPurchasePlan) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderChannels(b, mode, jwt)
|
||||
}
|
||||
|
||||
func (s *PlacementPurchasePlan) renderChannels(b *bot.Bot, mode bot.RenderMode, jwt string) {
|
||||
channels, err := b.Backend.GetPurchasePlanChannels(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
s.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get purchase plan channels")
|
||||
b.SendNew("❌ Не удалось загрузить каналы", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
text := "<b>📋 План закупа — Каналы</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(channels) == 0 {
|
||||
text += "Список каналов пуст.\n\n"
|
||||
} else {
|
||||
text += "Нажмите на канал для удаления:\n\n"
|
||||
|
||||
for _, ch := range channels {
|
||||
channelName := "Без названия"
|
||||
if ch.Channel.Title != nil {
|
||||
channelName = *ch.Channel.Title
|
||||
} else if ch.Channel.Username != nil {
|
||||
channelName = "@" + *ch.Channel.Username
|
||||
}
|
||||
|
||||
// Показываем статус и стоимость
|
||||
statusEmoji := "📌"
|
||||
switch ch.Status {
|
||||
case "planned":
|
||||
statusEmoji = "📝"
|
||||
case "approved":
|
||||
statusEmoji = "✅"
|
||||
case "rejected":
|
||||
statusEmoji = "❌"
|
||||
case "in_progress":
|
||||
statusEmoji = "🔄"
|
||||
case "completed":
|
||||
statusEmoji = "✔️"
|
||||
}
|
||||
|
||||
buttonText := fmt.Sprintf("%s %s", statusEmoji, channelName)
|
||||
if ch.PlannedCost != nil {
|
||||
buttonText += fmt.Sprintf(" (%.0f₽)", *ch.PlannedCost)
|
||||
}
|
||||
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(buttonText, fmt.Sprintf("remove:%s", ch.ID)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Инструкция по добавлению канала
|
||||
text += "\n<b>Добавление канала:</b>\nОтправьте username канала (без @)"
|
||||
|
||||
// Навигация
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
navRow = append(navRow, bot.Button("« Назад", "back"))
|
||||
navRow = append(navRow, bot.Button("Далее →", "next"))
|
||||
buttons = append(buttons, navRow)
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *PlacementPurchasePlan) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *PlacementPurchasePlan) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch {
|
||||
case data == "back":
|
||||
if s.AddPlacementState != nil {
|
||||
b.SetState(s.AddPlacementState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case data == "next":
|
||||
// TODO: переход к следующему шагу (детали размещения)
|
||||
b.Edit("🚧 Следующий шаг в разработке", bot.Keyboard(
|
||||
bot.Row(bot.Button("« Назад", "back_to_purchase_plan")),
|
||||
))
|
||||
|
||||
case data == "back_to_purchase_plan":
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case strings.HasPrefix(data, "remove:"):
|
||||
parts := strings.Split(data, ":")
|
||||
if len(parts) == 2 {
|
||||
purchasePlanChannelID := parts[1] // ID записи PurchasePlanChannel
|
||||
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем канал из плана закупа
|
||||
err = b.Backend.RemoveChannelFromPurchasePlan(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
s.ProjectID,
|
||||
purchasePlanChannelID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("purchase_plan_channel_id", purchasePlanChannelID).Msg("Failed to remove channel")
|
||||
b.Edit("❌ Не удалось удалить канал", bot.Keyboard(
|
||||
bot.Row(bot.Button("« Назад", "back_to_purchase_plan")),
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем экран
|
||||
s.Enter(b, bot.EditMessage)
|
||||
}
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PlacementPurchasePlan) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||||
if u.Message == nil || u.Message.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(u.Message.Text)
|
||||
|
||||
// Убираем @ если пользователь его добавил
|
||||
username = strings.TrimPrefix(username, "@")
|
||||
|
||||
if username == "" {
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
// Отправляем запрос на добавление канала
|
||||
input := backend.AttachChannelToPurchasePlanInput{
|
||||
Username: username,
|
||||
}
|
||||
|
||||
_, err = b.Backend.AttachChannelToPurchasePlan(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
s.ProjectID,
|
||||
input,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("username", username).Msg("Failed to attach channel")
|
||||
|
||||
// Проверяем, содержит ли ошибка информацию о том, что канал не найден
|
||||
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "404") {
|
||||
b.SendNew(fmt.Sprintf("❌ Канал @%s не найден в Telegram.\n\nПроверьте правильность username и попробуйте еще раз.", username), bot.Keyboard())
|
||||
} else {
|
||||
b.SendNew("❌ Не удалось добавить канал. Попробуйте позже.", bot.Keyboard())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем экран со списком каналов
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
|
||||
func (s *PlacementPurchasePlan) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
@@ -68,37 +68,32 @@ func (s *ProjectDetails) formatProjectDetails() string {
|
||||
func (s *ProjectDetails) buildKeyboard() echotron.InlineKeyboardMarkup {
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
// Первый ряд: Креативы и Создать закуп
|
||||
// Первый ряд: Креативы и Закупы
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("Креативы", fmt.Sprintf("creatives:%s", s.Project.ID)),
|
||||
bot.Button("Создать закуп", fmt.Sprintf("create_placement:%s", s.Project.ID)),
|
||||
bot.Button("✎ Креативы", fmt.Sprintf("creatives:%s", s.Project.ID)),
|
||||
bot.Button("☑ Закупы", fmt.Sprintf("purchases:%s", s.Project.ID)),
|
||||
))
|
||||
|
||||
// Второй ряд: План закупов
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("План закупов", fmt.Sprintf("purchase_plan:%s", s.Project.ID)),
|
||||
))
|
||||
|
||||
// Третий ряд: Тип приглашений (с подтверждением)
|
||||
// Второй ряд: Тип вступления по умолчанию для закупов (с подтверждением)
|
||||
var linkTypeButtonText string
|
||||
if s.confirmingLinkTypeChange {
|
||||
// Показываем подтверждение
|
||||
var newType string
|
||||
if s.Project.InviteLinkType == "public" {
|
||||
newType = "С одобрением"
|
||||
if s.Project.PurchaseInviteTypeDefault == "public" {
|
||||
newType = "По заявке"
|
||||
} else {
|
||||
newType = "Открытая"
|
||||
newType = "Свободное"
|
||||
}
|
||||
linkTypeButtonText = fmt.Sprintf("Подтвердить → %s", newType)
|
||||
linkTypeButtonText = fmt.Sprintf("Изменить → %s", newType)
|
||||
} else {
|
||||
// Показываем текущий тип приглашений
|
||||
// Показываем текущий тип вступления
|
||||
var currentType string
|
||||
if s.Project.InviteLinkType == "public" {
|
||||
currentType = "Открытая"
|
||||
if s.Project.PurchaseInviteTypeDefault == "public" {
|
||||
currentType = "Свободное"
|
||||
} else {
|
||||
currentType = "С одобрением"
|
||||
currentType = "По заявке"
|
||||
}
|
||||
linkTypeButtonText = fmt.Sprintf("Приглашения: %s", currentType)
|
||||
linkTypeButtonText = fmt.Sprintf("Вступление: %s", currentType)
|
||||
}
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)),
|
||||
@@ -135,12 +130,13 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
BackState: s,
|
||||
}, bot.NewMessage)
|
||||
|
||||
case strings.HasPrefix(data, "create_placement:"):
|
||||
case strings.HasPrefix(data, "purchases:"):
|
||||
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
|
||||
// TODO: реализовать создание закупа
|
||||
b.Edit("🚧 Создать закуп\n\nФункция в разработке", bot.Keyboard(
|
||||
bot.Row(bot.Button("← Назад", "back_to_project")),
|
||||
))
|
||||
b.SetState(&Purchases{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.Project.ID,
|
||||
BackState: s,
|
||||
}, bot.NewMessage)
|
||||
|
||||
case data == "back_to_project":
|
||||
s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения
|
||||
@@ -154,7 +150,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
} else {
|
||||
// Второе нажатие - меняем тип ссылки
|
||||
var newType string
|
||||
if s.Project.InviteLinkType == "public" {
|
||||
if s.Project.PurchaseInviteTypeDefault == "public" {
|
||||
newType = "approval"
|
||||
} else {
|
||||
newType = "public"
|
||||
@@ -163,7 +159,7 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
log.Info().
|
||||
Str("workspace_id", s.WorkspaceID).
|
||||
Str("project_id", s.Project.ID).
|
||||
Str("old_type", s.Project.InviteLinkType).
|
||||
Str("old_type", s.Project.PurchaseInviteTypeDefault).
|
||||
Str("new_type", newType).
|
||||
Msg("Updating project invite link type")
|
||||
|
||||
|
||||
170
telegram_bot/screens/purchase_details.go
Normal file
170
telegram_bot/screens/purchase_details.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PurchaseDetails struct {
|
||||
WorkspaceID string
|
||||
ProjectID string
|
||||
PurchaseID string
|
||||
BackState bot.State
|
||||
}
|
||||
|
||||
func (s *PurchaseDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderDetails(b, mode, jwt)
|
||||
}
|
||||
|
||||
func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt string) {
|
||||
purchase, err := b.Backend.GetPurchase(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
s.ProjectID,
|
||||
s.PurchaseID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get purchase")
|
||||
b.SendNew("❌ Не удалось загрузить закуп", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем информацию о креативе
|
||||
creative, err := b.Backend.GetCreative(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
purchase.CreativeID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get creative")
|
||||
creative = nil
|
||||
}
|
||||
|
||||
text := "<b>📦 Детали закупа</b>\n\n"
|
||||
|
||||
// Информация о креативе
|
||||
if creative != nil {
|
||||
text += fmt.Sprintf("<b>Креатив:</b> %s\n", creative.Name)
|
||||
} else {
|
||||
text += fmt.Sprintf("<b>Креатив ID:</b> %s\n", purchase.CreativeID)
|
||||
}
|
||||
|
||||
// Статус закупа
|
||||
statusText := "Активный"
|
||||
if purchase.Status == "archived" {
|
||||
statusText = "Архивирован"
|
||||
}
|
||||
text += fmt.Sprintf("<b>Статус:</b> %s\n\n", statusText)
|
||||
|
||||
// Информация о каналах
|
||||
if len(purchase.Channels) == 0 {
|
||||
text += "<i>Каналы не добавлены</i>\n"
|
||||
} else {
|
||||
text += fmt.Sprintf("<b>Каналы (%d):</b>\n\n", len(purchase.Channels))
|
||||
|
||||
for i, ch := range purchase.Channels {
|
||||
// Определяем статус emoji
|
||||
statusEmoji := "📌"
|
||||
switch ch.Status {
|
||||
case "planned":
|
||||
statusEmoji = "📝"
|
||||
case "approved":
|
||||
statusEmoji = "✅"
|
||||
case "rejected":
|
||||
statusEmoji = "❌"
|
||||
case "in_progress":
|
||||
statusEmoji = "🔄"
|
||||
case "completed":
|
||||
statusEmoji = "✔️"
|
||||
}
|
||||
|
||||
// Название канала
|
||||
channelName := "Без названия"
|
||||
if ch.Channel.Title != nil {
|
||||
channelName = *ch.Channel.Title
|
||||
} else if ch.Channel.Username != nil {
|
||||
channelName = "@" + *ch.Channel.Username
|
||||
}
|
||||
|
||||
text += fmt.Sprintf("%d. %s <b>%s</b>\n", i+1, statusEmoji, channelName)
|
||||
|
||||
// Стоимость
|
||||
if ch.PlannedCost != nil {
|
||||
text += fmt.Sprintf(" 💰 Стоимость: %.0f₽\n", *ch.PlannedCost)
|
||||
}
|
||||
|
||||
// Комментарий
|
||||
if ch.Comment != nil && *ch.Comment != "" {
|
||||
text += fmt.Sprintf(" 💬 %s\n", *ch.Comment)
|
||||
}
|
||||
|
||||
// Тип invite link
|
||||
inviteLinkTypeText := "публичная"
|
||||
if ch.InviteLinkType == "approval" {
|
||||
inviteLinkTypeText = "с одобрением"
|
||||
}
|
||||
text += fmt.Sprintf(" 🔗 Ссылка: %s\n", inviteLinkTypeText)
|
||||
|
||||
text += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
// Кнопка назад
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("← Назад", "back"),
|
||||
))
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *PurchaseDetails) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *PurchaseDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch data {
|
||||
case "back":
|
||||
if s.BackState != nil {
|
||||
b.SetState(s.BackState, bot.EditMessage)
|
||||
}
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PurchaseDetails) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *PurchaseDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
158
telegram_bot/screens/purchases.go
Normal file
158
telegram_bot/screens/purchases.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Purchases struct {
|
||||
WorkspaceID string
|
||||
ProjectID string
|
||||
BackState bot.State
|
||||
}
|
||||
|
||||
func (s *Purchases) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderPurchases(b, mode, jwt)
|
||||
}
|
||||
|
||||
func (s *Purchases) renderPurchases(b *bot.Bot, mode bot.RenderMode, jwt string) {
|
||||
purchases, err := b.Backend.GetPurchases(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
s.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get purchases")
|
||||
b.SendNew("❌ Не удалось загрузить закупы", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
text := "<b>📦 Закупы</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(purchases) == 0 {
|
||||
text += `Список закупов пуст.
|
||||
|
||||
<i>Закуп — это план размещения рекламы в каналах:</i>
|
||||
▸ Выбор креатива
|
||||
▸ Список каналов для размещения
|
||||
▸ Планируемая стоимость
|
||||
▸ Статус выполнения
|
||||
|
||||
<i>Создайте первый закуп 👇</i>`
|
||||
} else {
|
||||
text += "Управление закупами для проекта.\n\n"
|
||||
text += "<i>Выберите закуп для деталей:</i>\n\n"
|
||||
|
||||
for i, purchase := range purchases {
|
||||
// Определяем статус emoji
|
||||
statusEmoji := "📌"
|
||||
switch purchase.Status {
|
||||
case "active":
|
||||
statusEmoji = "🟢"
|
||||
case "archived":
|
||||
statusEmoji = "📦"
|
||||
}
|
||||
|
||||
// Считаем каналы по статусам
|
||||
totalChannels := len(purchase.Channels)
|
||||
completedChannels := 0
|
||||
for _, ch := range purchase.Channels {
|
||||
if ch.Status == "completed" {
|
||||
completedChannels++
|
||||
}
|
||||
}
|
||||
|
||||
buttonText := fmt.Sprintf("%s Закуп #%d (%d/%d каналов)",
|
||||
statusEmoji,
|
||||
i+1,
|
||||
completedChannels,
|
||||
totalChannels,
|
||||
)
|
||||
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(buttonText, fmt.Sprintf("purchase:%s", purchase.ID)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка создания нового закупа
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("+ Создать закуп", "add_purchase"),
|
||||
))
|
||||
|
||||
// Кнопка назад
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("← Назад", "back"),
|
||||
))
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *Purchases) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *Purchases) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch data {
|
||||
case "back":
|
||||
if s.BackState != nil {
|
||||
b.SetState(s.BackState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case "add_purchase":
|
||||
b.SetState(&AddPurchase{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.ProjectID,
|
||||
BackState: s,
|
||||
}, bot.EditMessage)
|
||||
|
||||
default:
|
||||
// Проверяем, не нажали ли на конкретный закуп
|
||||
if len(data) > 9 && data[:9] == "purchase:" {
|
||||
purchaseID := data[9:]
|
||||
b.SetState(&PurchaseDetails{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.ProjectID,
|
||||
PurchaseID: purchaseID,
|
||||
BackState: s,
|
||||
}, bot.NewMessage)
|
||||
} else {
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Purchases) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *Purchases) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
214
telegram_bot/screens/select_channels_for_purchase.go
Normal file
214
telegram_bot/screens/select_channels_for_purchase.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"example.com/m/adapter/backend"
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PurchaseChannelInput struct {
|
||||
Username string
|
||||
PlannedCost *float64
|
||||
Comment *string
|
||||
}
|
||||
|
||||
type SelectChannelsForPurchase struct {
|
||||
WorkspaceID string
|
||||
ProjectID string
|
||||
CreativeID string
|
||||
Channels []PurchaseChannelInput
|
||||
BackState bot.State
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
s.renderChannelSelection(b, mode)
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.RenderMode) {
|
||||
text := "<b>➕ Создание закупа</b>\n\n"
|
||||
text += "<b>Шаг 2/2: Добавление каналов</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(s.Channels) == 0 {
|
||||
text += `Добавьте каналы для размещения рекламы.
|
||||
|
||||
<i>Отправьте username канала (без @)</i>
|
||||
Например: <code>channel_name</code>
|
||||
|
||||
После добавления всех каналов нажмите "Создать закуп"`
|
||||
} else {
|
||||
text += fmt.Sprintf("<b>Добавлено каналов: %d</b>\n\n", len(s.Channels))
|
||||
|
||||
for i, ch := range s.Channels {
|
||||
channelText := fmt.Sprintf("%d. @%s", i+1, ch.Username)
|
||||
if ch.PlannedCost != nil {
|
||||
channelText += fmt.Sprintf(" (%.0f₽)", *ch.PlannedCost)
|
||||
}
|
||||
text += channelText + "\n"
|
||||
}
|
||||
|
||||
text += "\n<i>Добавьте еще каналы или создайте закуп</i>"
|
||||
|
||||
// Кнопка создания закупа (доступна только если есть каналы)
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("✅ Создать закуп", "create"),
|
||||
))
|
||||
}
|
||||
|
||||
// Кнопки навигации
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("← Назад", "back"),
|
||||
))
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch data {
|
||||
case "back":
|
||||
// Возвращаемся к выбору креатива
|
||||
b.SetState(&AddPurchase{
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ProjectID: s.ProjectID,
|
||||
BackState: s.BackState,
|
||||
}, bot.EditMessage)
|
||||
|
||||
case "create":
|
||||
if len(s.Channels) == 0 {
|
||||
b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard(
|
||||
bot.Row(bot.Button("← Назад", "back_to_select")),
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.Edit("❌ Ошибка авторизации", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем закуп
|
||||
s.createPurchase(b, jwt)
|
||||
|
||||
case "back_to_select":
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
|
||||
// Преобразуем каналы в формат API
|
||||
var apiChannels []backend.CreatePurchaseChannelInput
|
||||
for _, ch := range s.Channels {
|
||||
apiChannels = append(apiChannels, backend.CreatePurchaseChannelInput{
|
||||
Username: ch.Username,
|
||||
PlannedCost: ch.PlannedCost,
|
||||
Comment: ch.Comment,
|
||||
})
|
||||
}
|
||||
|
||||
input := backend.CreatePurchaseInput{
|
||||
CreativeID: s.CreativeID,
|
||||
Channels: apiChannels,
|
||||
}
|
||||
|
||||
purchase, err := b.Backend.CreatePurchase(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
s.ProjectID,
|
||||
input,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create purchase")
|
||||
b.Edit("❌ Не удалось создать закуп\n\nВозможные причины:\n• Один из каналов не найден\n• Ошибка сервера", bot.Keyboard(
|
||||
bot.Row(bot.Button("← Назад", "back_to_select")),
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// Показываем успешное создание
|
||||
successText := fmt.Sprintf(`✅ <b>Закуп создан!</b>
|
||||
|
||||
<b>ID:</b> %s
|
||||
<b>Креатив:</b> %s
|
||||
<b>Каналов:</b> %d
|
||||
|
||||
Закуп создан и готов к работе.`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
|
||||
|
||||
b.Edit(successText, bot.Keyboard(
|
||||
bot.Row(bot.Button("✅ Готово", "done")),
|
||||
))
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||||
if u.Message == nil || u.Message.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(u.Message.Text)
|
||||
|
||||
// Убираем @ если пользователь его добавил
|
||||
username = strings.TrimPrefix(username, "@")
|
||||
|
||||
if username == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, не добавлен ли уже этот канал
|
||||
for _, ch := range s.Channels {
|
||||
if ch.Username == username {
|
||||
b.SendNew(fmt.Sprintf("⚠️ Канал @%s уже добавлен в список", username), bot.Keyboard())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем канал в список
|
||||
s.Channels = append(s.Channels, PurchaseChannelInput{
|
||||
Username: username,
|
||||
})
|
||||
|
||||
// Обновляем экран
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) Handle(b *bot.Bot, u *echotron.Update) {
|
||||
// Обрабатываем callback после успешного создания
|
||||
if u.CallbackQuery != nil && u.CallbackQuery.Data == "done" {
|
||||
// Возвращаемся к списку закупов
|
||||
if s.BackState != nil {
|
||||
b.SetState(s.BackState, bot.EditMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const creativesForPlacementPerPage = 6
|
||||
|
||||
type SelectCreativeForPlacement struct {
|
||||
WorkspaceID string
|
||||
ProjectID string
|
||||
CurrentPage int
|
||||
AddPlacementState *AddPlacement
|
||||
}
|
||||
|
||||
func (s *SelectCreativeForPlacement) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderCreatives(b, mode, jwt)
|
||||
}
|
||||
|
||||
func (s *SelectCreativeForPlacement) renderCreatives(b *bot.Bot, mode bot.RenderMode, jwt string) {
|
||||
page, err := b.Backend.GetCreatives(
|
||||
context.Background(),
|
||||
jwt,
|
||||
s.WorkspaceID,
|
||||
&s.ProjectID,
|
||||
false, // includeArchived
|
||||
s.CurrentPage+1,
|
||||
creativesForPlacementPerPage,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get creatives")
|
||||
b.SendNew("❌ Не удалось загрузить креативы", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
text := "<b>Выбор креатива</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(page.Items) == 0 {
|
||||
text += "У выбранного проекта пока нет креативов.\n\nСоздайте креатив через меню проекта."
|
||||
} else {
|
||||
text += fmt.Sprintf("Выберите креатив (стр. %d из %d):", s.CurrentPage+1, page.Pages)
|
||||
|
||||
// Креативы по 2 в ряду
|
||||
for i := 0; i < len(page.Items); i += 2 {
|
||||
var row []echotron.InlineKeyboardButton
|
||||
|
||||
// Первый креатив в ряду
|
||||
creative := page.Items[i]
|
||||
row = append(row, bot.Button(creative.Name, fmt.Sprintf("select:%s", creative.ID)))
|
||||
|
||||
// Второй креатив в ряду (если есть)
|
||||
if i+1 < len(page.Items) {
|
||||
creative = page.Items[i+1]
|
||||
row = append(row, bot.Button(creative.Name, fmt.Sprintf("select:%s", creative.ID)))
|
||||
} else {
|
||||
row = append(row, bot.Button(" ", "empty"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, row)
|
||||
}
|
||||
|
||||
// Кнопки пагинации
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
|
||||
if s.CurrentPage > 0 {
|
||||
navRow = append(navRow, bot.Button("←", "prev"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button("·", "empty"))
|
||||
}
|
||||
|
||||
pageButton := bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, page.Pages), "page_info")
|
||||
navRow = append(navRow, pageButton)
|
||||
|
||||
if s.CurrentPage+1 < page.Pages {
|
||||
navRow = append(navRow, bot.Button("→", "next"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button("·", "empty"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, navRow)
|
||||
}
|
||||
|
||||
// Опция пропустить выбор креатива (креатив опционален)
|
||||
buttons = append(buttons, bot.Row(bot.Button("Без креатива", "skip")))
|
||||
|
||||
// Кнопка возврата
|
||||
buttons = append(buttons, bot.Row(bot.Button("« Назад", "back")))
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *SelectCreativeForPlacement) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *SelectCreativeForPlacement) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch {
|
||||
case data == "prev":
|
||||
if s.CurrentPage > 0 {
|
||||
s.CurrentPage--
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case data == "next":
|
||||
s.CurrentPage++
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case data == "page_info", data == "empty":
|
||||
return
|
||||
|
||||
case data == "skip":
|
||||
// Пропускаем выбор креатива
|
||||
if s.AddPlacementState != nil {
|
||||
s.AddPlacementState.SelectedCreative = nil
|
||||
b.SetState(s.AddPlacementState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case data == "back":
|
||||
if s.AddPlacementState != nil {
|
||||
b.SetState(s.AddPlacementState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(data, "select:"):
|
||||
parts := strings.Split(data, ":")
|
||||
if len(parts) == 2 {
|
||||
creativeID := parts[1]
|
||||
|
||||
// Получаем JWT
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
return
|
||||
}
|
||||
|
||||
// Загружаем полные данные креатива
|
||||
creative, err := b.Backend.GetCreative(context.Background(), jwt, s.WorkspaceID, creativeID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("creative_id", creativeID).Msg("Failed to get creative")
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем выбранный креатив в состояние AddPlacement
|
||||
if s.AddPlacementState != nil {
|
||||
s.AddPlacementState.SelectedCreative = creative
|
||||
b.SetState(s.AddPlacementState, bot.EditMessage)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SelectCreativeForPlacement) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *SelectCreativeForPlacement) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
@@ -1,164 +0,0 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"example.com/m/bot"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const projectsForPlacementPerPage = 6
|
||||
|
||||
type SelectProjectForPlacement struct {
|
||||
WorkspaceID string
|
||||
CurrentPage int
|
||||
AddPlacementState *AddPlacement
|
||||
}
|
||||
|
||||
func (s *SelectProjectForPlacement) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
jwt, err := s.ensureJWT(b)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get JWT")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderProjects(b, mode, jwt)
|
||||
}
|
||||
|
||||
func (s *SelectProjectForPlacement) renderProjects(b *bot.Bot, mode bot.RenderMode, jwt string) {
|
||||
page, err := b.Backend.GetProjects(context.Background(), jwt, s.WorkspaceID, s.CurrentPage+1, projectsForPlacementPerPage)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get projects")
|
||||
b.SendNew("❌ Не удалось загрузить проекты", bot.Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
text := "<b>Выбор проекта</b>\n\n"
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
if len(page.Items) == 0 {
|
||||
text += "У вас пока нет проектов"
|
||||
} else {
|
||||
text += fmt.Sprintf("Выберите проект (стр. %d из %d):", s.CurrentPage+1, page.Pages)
|
||||
|
||||
// Проекты по 2 в ряду
|
||||
for i := 0; i < len(page.Items); i += 2 {
|
||||
var row []echotron.InlineKeyboardButton
|
||||
|
||||
// Первый проект в ряду
|
||||
project := page.Items[i]
|
||||
row = append(row, bot.Button(project.Title, fmt.Sprintf("select:%s", project.ID)))
|
||||
|
||||
// Второй проект в ряду (если есть)
|
||||
if i+1 < len(page.Items) {
|
||||
project = page.Items[i+1]
|
||||
row = append(row, bot.Button(project.Title, fmt.Sprintf("select:%s", project.ID)))
|
||||
} else {
|
||||
row = append(row, bot.Button(" ", "empty"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, row)
|
||||
}
|
||||
|
||||
// Кнопки пагинации
|
||||
var navRow []echotron.InlineKeyboardButton
|
||||
|
||||
if s.CurrentPage > 0 {
|
||||
navRow = append(navRow, bot.Button("←", "prev"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button("·", "empty"))
|
||||
}
|
||||
|
||||
pageButton := bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, page.Pages), "page_info")
|
||||
navRow = append(navRow, pageButton)
|
||||
|
||||
if s.CurrentPage+1 < page.Pages {
|
||||
navRow = append(navRow, bot.Button("→", "next"))
|
||||
} else {
|
||||
navRow = append(navRow, bot.Button("·", "empty"))
|
||||
}
|
||||
|
||||
buttons = append(buttons, navRow)
|
||||
}
|
||||
|
||||
// Кнопка возврата
|
||||
buttons = append(buttons, bot.Row(bot.Button("« Назад", "back")))
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Render(text, keyboard, mode)
|
||||
}
|
||||
|
||||
func (s *SelectProjectForPlacement) ensureJWT(b *bot.Bot) (string, error) {
|
||||
if b.Session.JWT != "" {
|
||||
return b.Session.JWT, nil
|
||||
}
|
||||
|
||||
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.Session.JWT = jwt
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
func (s *SelectProjectForPlacement) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data := u.CallbackQuery.Data
|
||||
|
||||
switch {
|
||||
case data == "prev":
|
||||
if s.CurrentPage > 0 {
|
||||
s.CurrentPage--
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case data == "next":
|
||||
s.CurrentPage++
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case data == "page_info", data == "empty":
|
||||
return
|
||||
|
||||
case data == "back":
|
||||
if s.AddPlacementState != nil {
|
||||
b.SetState(s.AddPlacementState, bot.EditMessage)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(data, "select:"):
|
||||
parts := strings.Split(data, ":")
|
||||
if len(parts) == 2 {
|
||||
projectID := parts[1]
|
||||
|
||||
// Загружаем полные данные проекта
|
||||
project, err := b.Backend.GetProject(context.Background(), s.WorkspaceID, projectID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("project_id", projectID).Msg("Failed to get project")
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем выбранный проект в состояние AddPlacement
|
||||
if s.AddPlacementState != nil {
|
||||
s.AddPlacementState.SelectedProject = project
|
||||
// Сбрасываем креатив при смене проекта
|
||||
s.AddPlacementState.SelectedCreative = nil
|
||||
b.SetState(s.AddPlacementState, bot.EditMessage)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SelectProjectForPlacement) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *SelectProjectForPlacement) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
Reference in New Issue
Block a user