правки
This commit is contained in:
@@ -397,6 +397,17 @@ class Postgres(DatabaseBase):
|
|||||||
'project', 'project__channel', 'channel', 'creative'
|
'project', 'project__channel', 'channel', 'creative'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def count_placements_by_project_and_channel(
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
) -> int:
|
||||||
|
"""Count placements for a project in a specific channel."""
|
||||||
|
return await domain.Placement.filter(
|
||||||
|
project_id=project_id,
|
||||||
|
channel_id=channel_id,
|
||||||
|
).count()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_placement(placement: domain.Placement) -> None:
|
async def update_placement(placement: domain.Placement) -> None:
|
||||||
await placement.save()
|
await placement.save()
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ from src.domain.placement_post import PlacementPostStatus
|
|||||||
from .channel import ChannelOutput
|
from .channel import ChannelOutput
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectOutput(pydantic.BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
status: str
|
||||||
|
channel: ChannelOutput
|
||||||
|
|
||||||
|
|
||||||
class CostInfo(pydantic.BaseModel):
|
class CostInfo(pydantic.BaseModel):
|
||||||
type: CostType
|
type: CostType
|
||||||
value: float
|
value: float
|
||||||
@@ -30,11 +36,14 @@ class PlacementOutput(pydantic.BaseModel):
|
|||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
status: PlacementStatus
|
status: PlacementStatus
|
||||||
creative_id: uuid.UUID | None = None
|
creative_id: uuid.UUID | None = None
|
||||||
|
creative_name: str | None = None
|
||||||
comment: str | None = None
|
comment: str | None = None
|
||||||
invite_link: str | None
|
invite_link: str | None
|
||||||
invite_link_created_at: datetime.datetime | None = None
|
invite_link_created_at: datetime.datetime | None = None
|
||||||
invite_link_type: InviteLinkType
|
invite_link_type: InviteLinkType
|
||||||
channel: ChannelOutput
|
channel: ChannelOutput
|
||||||
|
project: ProjectOutput | None = None
|
||||||
|
placement_number: int
|
||||||
details: PlacementDetails | None = None
|
details: PlacementDetails | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,16 +31,41 @@ def _build_placement_details(placement: domain.Placement) -> dto.PlacementDetail
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput:
|
def _build_placement_output(
|
||||||
|
placement: domain.Placement,
|
||||||
|
project: domain.Project | None = None,
|
||||||
|
placement_number: int = 0,
|
||||||
|
creative_name: str | None = None,
|
||||||
|
) -> dto.PlacementOutput:
|
||||||
channel = placement.channel
|
channel = placement.channel
|
||||||
if channel is None:
|
if channel is None:
|
||||||
log.error('Placement %s has no channel prefetched', placement.id)
|
log.error('Placement %s has no channel prefetched', placement.id)
|
||||||
raise ValueError(f'Placement {placement.id} has no channel')
|
raise ValueError(f'Placement {placement.id} has no channel')
|
||||||
|
|
||||||
|
# Build project output if project is provided
|
||||||
|
project_output: dto.ProjectOutput | None = None
|
||||||
|
if project is not None:
|
||||||
|
project_channel = project.channel
|
||||||
|
if project_channel is None:
|
||||||
|
log.error('Project %s has no channel', project.id)
|
||||||
|
raise ValueError(f'Project {project.id} has no channel')
|
||||||
|
|
||||||
|
project_output = dto.ProjectOutput(
|
||||||
|
id=project.id,
|
||||||
|
status=project.status.value,
|
||||||
|
channel=dto.ChannelOutput(
|
||||||
|
id=project_channel.id,
|
||||||
|
telegram_id=project_channel.telegram_id,
|
||||||
|
title=project_channel.title,
|
||||||
|
username=project_channel.username,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return dto.PlacementOutput(
|
return dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
status=placement.status,
|
status=placement.status,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
|
creative_name=creative_name,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
invite_link=placement.invite_link,
|
invite_link=placement.invite_link,
|
||||||
invite_link_type=placement.invite_link_type,
|
invite_link_type=placement.invite_link_type,
|
||||||
@@ -50,6 +75,8 @@ def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput:
|
|||||||
title=channel.title,
|
title=channel.title,
|
||||||
username=channel.username,
|
username=channel.username,
|
||||||
),
|
),
|
||||||
|
project=project_output,
|
||||||
|
placement_number=placement_number,
|
||||||
details=_build_placement_details(placement),
|
details=_build_placement_details(placement),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,7 +100,6 @@ async def create_placements(
|
|||||||
if project.channel.telegram_id is None:
|
if project.channel.telegram_id is None:
|
||||||
raise domain.ChannelNotFound(project.channel.id)
|
raise domain.ChannelNotFound(project.channel.id)
|
||||||
|
|
||||||
invite_link_type = project.purchase_invite_type_default
|
|
||||||
details = input.details
|
details = input.details
|
||||||
placements: list[domain.Placement] = []
|
placements: list[domain.Placement] = []
|
||||||
creatives_by_id: dict[uuid.UUID, domain.Creative] = {}
|
creatives_by_id: dict[uuid.UUID, domain.Creative] = {}
|
||||||
@@ -106,7 +132,9 @@ async def create_placements(
|
|||||||
creative_id=creative.id if creative else None,
|
creative_id=creative.id if creative else None,
|
||||||
channel_id=channel.id,
|
channel_id=channel.id,
|
||||||
invite_link=None,
|
invite_link=None,
|
||||||
invite_link_type=invite_link_type,
|
invite_link_type=(channel_details.invite_link_type if channel_details and channel_details.invite_link_type else None)
|
||||||
|
or (details.invite_link_type if details and details.invite_link_type else None)
|
||||||
|
or project.purchase_invite_type_default,
|
||||||
status=channel_input.status or domain.PlacementStatus.NO_STATUS,
|
status=channel_input.status or domain.PlacementStatus.NO_STATUS,
|
||||||
comment=channel_input.comment,
|
comment=channel_input.comment,
|
||||||
# Приоритет: channel details > общие details
|
# Приоритет: channel details > общие details
|
||||||
@@ -138,7 +166,7 @@ async def create_placements(
|
|||||||
|
|
||||||
placement_outputs = []
|
placement_outputs = []
|
||||||
for placement in placements:
|
for placement in placements:
|
||||||
placement_output = _build_placement_output(placement)
|
placement_output = _build_placement_output(placement, project=project)
|
||||||
placement_outputs.append(
|
placement_outputs.append(
|
||||||
dto.PlacementWithPostsOutput(
|
dto.PlacementWithPostsOutput(
|
||||||
**placement_output.model_dump(),
|
**placement_output.model_dump(),
|
||||||
|
|||||||
@@ -77,6 +77,30 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
|
|||||||
channel = await self.database.get_channel(channel_id=placement.channel_id)
|
channel = await self.database.get_channel(channel_id=placement.channel_id)
|
||||||
placement.channel = channel
|
placement.channel = channel
|
||||||
|
|
||||||
|
# Prefetch project channel for output building
|
||||||
|
if project.channel is None:
|
||||||
|
project_channel = await self.database.get_channel(channel_id=project.channel_id)
|
||||||
|
project.channel = project_channel
|
||||||
|
|
||||||
|
# Get creative name if exists
|
||||||
|
creative_name = None
|
||||||
|
if placement.creative_id:
|
||||||
|
creative = await self.database.get_creative(input.workspace_id, placement.creative_id)
|
||||||
|
if creative:
|
||||||
|
creative_name = creative.name
|
||||||
|
|
||||||
|
# Calculate placement number (ordinal number of this placement)
|
||||||
|
placement_number = await self.database.count_placements_by_project_and_channel(
|
||||||
|
project.id, placement.channel_id
|
||||||
|
)
|
||||||
|
# Count placements created before this one
|
||||||
|
placements_before = await domain.Placement.filter(
|
||||||
|
project_id=project.id,
|
||||||
|
channel_id=placement.channel_id,
|
||||||
|
created_at__lt=placement.created_at,
|
||||||
|
).count()
|
||||||
|
placement_number = placements_before + 1
|
||||||
|
|
||||||
placement_posts = await self.database.get_workspace_placement_posts(
|
placement_posts = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
placement_id=placement.id,
|
placement_id=placement.id,
|
||||||
@@ -112,7 +136,12 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
|
|||||||
if placement_post_output is not None:
|
if placement_post_output is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
placement_output = _build_placement_output(placement)
|
placement_output = _build_placement_output(
|
||||||
|
placement,
|
||||||
|
project=project,
|
||||||
|
placement_number=placement_number,
|
||||||
|
creative_name=creative_name,
|
||||||
|
)
|
||||||
return dto.PlacementWithPostsOutput(
|
return dto.PlacementWithPostsOutput(
|
||||||
**placement_output.model_dump(),
|
**placement_output.model_dump(),
|
||||||
placement_post=placement_post_output,
|
placement_post=placement_post_output,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
|
|
||||||
placement_outputs = []
|
placement_outputs = []
|
||||||
for placement in placements:
|
for placement in placements:
|
||||||
placement_output = _build_placement_output(placement)
|
placement_output = _build_placement_output(placement, project=project)
|
||||||
placement_post_output = None
|
placement_post_output = None
|
||||||
placement_posts_for_placement = placement_posts_by_placement_id.get(placement.id, [])
|
placement_posts_for_placement = placement_posts_by_placement_id.get(placement.id, [])
|
||||||
if placement_posts_for_placement:
|
if placement_posts_for_placement:
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ type Channel struct {
|
|||||||
TelegramID *int64 `json:"telegram_id"`
|
TelegramID *int64 `json:"telegram_id"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
|
InviteLink *string `json:"invite_link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateChannelInput struct {
|
type CreateChannelInput struct {
|
||||||
@@ -568,6 +569,12 @@ type CreateChannelResult struct {
|
|||||||
Error *string `json:"error,omitempty"`
|
Error *string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectOutput struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Channel Channel `json:"channel"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateChannelsOutput struct {
|
type CreateChannelsOutput struct {
|
||||||
Results []CreateChannelResult `json:"results"`
|
Results []CreateChannelResult `json:"results"`
|
||||||
}
|
}
|
||||||
@@ -586,24 +593,30 @@ type PlacementDetails struct {
|
|||||||
Format *string `json:"format,omitempty"`
|
Format *string `json:"format,omitempty"`
|
||||||
Comment *string `json:"comment,omitempty"`
|
Comment *string `json:"comment,omitempty"`
|
||||||
CreativeID *string `json:"creative_id,omitempty"`
|
CreativeID *string `json:"creative_id,omitempty"`
|
||||||
|
CreativeName *string `json:"creative_name,omitempty"`
|
||||||
|
InviteLinkType *string `json:"invite_link_type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlacementOutput struct {
|
type PlacementOutput struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreativeID *string `json:"creative_id,omitempty"`
|
CreativeID *string `json:"creative_id,omitempty"`
|
||||||
Comment *string `json:"comment,omitempty"`
|
CreativeName *string `json:"creative_name,omitempty"`
|
||||||
InviteLink *string `json:"invite_link,omitempty"`
|
Comment *string `json:"comment,omitempty"`
|
||||||
InviteLinkType string `json:"invite_link_type"`
|
InviteLink *string `json:"invite_link,omitempty"`
|
||||||
Channel Channel `json:"channel"`
|
InviteLinkType string `json:"invite_link_type"`
|
||||||
Details *PlacementDetails `json:"details,omitempty"`
|
Channel Channel `json:"channel"`
|
||||||
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
|
Project *ProjectOutput `json:"project"`
|
||||||
|
PlacementNumber int `json:"placement_number"`
|
||||||
|
Details *PlacementDetails `json:"details,omitempty"`
|
||||||
|
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlacementPostOutput struct {
|
type PlacementPostOutput struct {
|
||||||
SubscriptionsCount int `json:"subscriptions_count"`
|
SubscriptionsCount int `json:"subscriptions_count"`
|
||||||
ViewsCount *int `json:"views_count,omitempty"`
|
ViewsCount *int `json:"views_count,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
TimeOnTop *int `json:"time_on_top,omitempty"`
|
||||||
Post PostOutput `json:"post"`
|
Post PostOutput `json:"post"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,62 +32,164 @@ func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
|
|
||||||
text := "<b>Детали размещения</b>\n\n"
|
text := "<b>Детали размещения</b>\n\n"
|
||||||
|
|
||||||
channelName := formatChannelName(placement.Channel)
|
// Размещение в канале
|
||||||
text += fmt.Sprintf("• <b>Канал:</b> %s\n", channelName)
|
text += "📺 <b>Размещение в канале:</b>\n"
|
||||||
text += fmt.Sprintf("• <b>Статус:</b> %s\n", formatPlacementStatus(placement.Status))
|
text += formatChannelWithName(placement.Channel)
|
||||||
text += fmt.Sprintf("• <b>Ссылка:</b> %s\n", formatInviteLinkType(placement.InviteLinkType))
|
text += "\n"
|
||||||
|
|
||||||
|
// Рекламируемый проект
|
||||||
|
if placement.Project != nil {
|
||||||
|
text += "🎯 <b>Рекламируемый проект:</b>\n"
|
||||||
|
text += formatProject(*placement.Project)
|
||||||
|
text += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ссылка на пост
|
||||||
|
if placementPost != nil && placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
|
||||||
|
text += fmt.Sprintf("🔗 <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пригласительная ссылка
|
||||||
|
if placement.InviteLink != nil && *placement.InviteLink != "" {
|
||||||
|
text += fmt.Sprintf("📩 <b>Пригласительная ссылка:</b> %s\n", *placement.InviteLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тип ссылки
|
||||||
|
text += fmt.Sprintf("🔓 <b>Тип ссылки:</b> %s\n", formatInviteLinkType(placement.InviteLinkType))
|
||||||
|
|
||||||
|
// Порядковый номер размещения
|
||||||
|
if placement.PlacementNumber > 0 {
|
||||||
|
text += fmt.Sprintf("🔢 <b>Размещение №%d</b> в этом канале\n", placement.PlacementNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Креатив
|
||||||
|
if placement.Details != nil && placement.Details.CreativeName != nil && *placement.Details.CreativeName != "" {
|
||||||
|
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.Details.CreativeName)
|
||||||
|
} else if placement.CreativeName != nil && *placement.CreativeName != "" {
|
||||||
|
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.CreativeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
// Финансовая информация
|
||||||
if placement.Details != nil {
|
if placement.Details != nil {
|
||||||
if placement.Details.PlacementType != nil {
|
// Стоимость
|
||||||
text += fmt.Sprintf("• <b>Тип:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
|
|
||||||
}
|
|
||||||
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
|
|
||||||
text += fmt.Sprintf("• <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
|
|
||||||
}
|
|
||||||
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
|
|
||||||
text += fmt.Sprintf("• <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
|
|
||||||
}
|
|
||||||
if placement.Details.Cost != nil {
|
if placement.Details.Cost != nil {
|
||||||
text += fmt.Sprintf("• <b>Стоимость:</b> %s %.0f₽\n",
|
costType := formatCostType(placement.Details.Cost.Type)
|
||||||
formatCostType(placement.Details.Cost.Type),
|
text += fmt.Sprintf("💰 <b>Стоимость:</b> %s %.0f₽\n", costType, placement.Details.Cost.Value)
|
||||||
placement.Details.Cost.Value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Стоимость до торга
|
||||||
if placement.Details.CostBeforeBargain != nil {
|
if placement.Details.CostBeforeBargain != nil {
|
||||||
text += fmt.Sprintf("• <b>До торга:</b> %s %.0f₽\n",
|
costType := formatCostType(placement.Details.CostBeforeBargain.Type)
|
||||||
formatCostType(placement.Details.CostBeforeBargain.Type),
|
text += fmt.Sprintf("💸 <b>До торга:</b> %s %.0f₽\n", costType, placement.Details.CostBeforeBargain.Value)
|
||||||
placement.Details.CostBeforeBargain.Value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Дата размещения
|
||||||
|
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
|
||||||
|
text += fmt.Sprintf("📅 <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дата оплаты
|
||||||
|
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
|
||||||
|
text += fmt.Sprintf("💳 <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формат
|
||||||
if placement.Details.Format != nil && *placement.Details.Format != "" {
|
if placement.Details.Format != nil && *placement.Details.Format != "" {
|
||||||
text += fmt.Sprintf("• <b>Формат:</b> %s\n", *placement.Details.Format)
|
text += fmt.Sprintf("📐 <b>Формат:</b> %s\n", *placement.Details.Format)
|
||||||
}
|
}
|
||||||
if placement.Details.Comment != nil && *placement.Details.Comment != "" {
|
|
||||||
text += fmt.Sprintf("• <b>Комментарий:</b> %s\n", *placement.Details.Comment)
|
// Тип закупа
|
||||||
|
if placement.Details.PlacementType != nil {
|
||||||
|
text += fmt.Sprintf("🔄 <b>Тип закупа:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
// Статистика
|
||||||
if placementPost != nil {
|
if placementPost != nil {
|
||||||
text += "\n<b>Пост</b>\n"
|
// Кол-во подписчиков
|
||||||
if placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
|
if placementPost.SubscriptionsCount > 0 {
|
||||||
text += fmt.Sprintf("• <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
|
text += fmt.Sprintf("👥 <b>Подписчики:</b> %d\n", placementPost.SubscriptionsCount)
|
||||||
} else {
|
|
||||||
text += "• <b>Ссылка на пост:</b> не найдена\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Просмотры поста
|
||||||
|
if placementPost.ViewsCount != nil {
|
||||||
|
text += fmt.Sprintf("👁️ <b>Просмотры:</b> %d\n", *placementPost.ViewsCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPF (стоимость подписчика)
|
||||||
|
if placementPost.SubscriptionsCount > 0 && placement.Details != nil && placement.Details.Cost != nil {
|
||||||
|
cpf := calculateCPF(placement.Details.Cost.Value, placementPost.SubscriptionsCount)
|
||||||
|
text += fmt.Sprintf("💵 <b>CPF:</b> %.2f₽\n", cpf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Конверсия
|
||||||
|
if placementPost.ViewsCount != nil && *placementPost.ViewsCount > 0 && placementPost.SubscriptionsCount > 0 {
|
||||||
|
conversion := calculateConversion(placementPost.SubscriptionsCount, *placementPost.ViewsCount)
|
||||||
|
text += fmt.Sprintf("📊 <b>Конверсия:</b> %.1f%%\n", conversion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Комментарий
|
||||||
|
if placement.Details != nil && placement.Details.Comment != nil && *placement.Details.Comment != "" {
|
||||||
|
text += fmt.Sprintf("\n📝 <b>Комментарий:</b> %s\n", *placement.Details.Comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard := Keyboard(Row(Button("← Назад", "back")))
|
keyboard := Keyboard(Row(Button("← Назад", "back")))
|
||||||
b.Render(text, keyboard, mode)
|
b.Render(text, keyboard, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatChannelName(channel backend.Channel) string {
|
func formatChannelWithName(channel backend.Channel) string {
|
||||||
|
var name string
|
||||||
if channel.Title != nil && *channel.Title != "" {
|
if channel.Title != nil && *channel.Title != "" {
|
||||||
return *channel.Title
|
name = *channel.Title
|
||||||
|
} else if channel.Username != nil && *channel.Username != "" {
|
||||||
|
name = "@" + *channel.Username
|
||||||
|
} else {
|
||||||
|
name = "Без названия"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем ссылку если есть username или invite_link
|
||||||
if channel.Username != nil && *channel.Username != "" {
|
if channel.Username != nil && *channel.Username != "" {
|
||||||
return "@" + *channel.Username
|
return fmt.Sprintf(" %s (@%s)\n", name, *channel.Username)
|
||||||
|
} else if channel.InviteLink != nil && *channel.InviteLink != "" {
|
||||||
|
return fmt.Sprintf(" %s\n", name)
|
||||||
}
|
}
|
||||||
return "Без названия"
|
return fmt.Sprintf(" %s\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatProject(project backend.ProjectOutput) string {
|
||||||
|
var name string
|
||||||
|
if project.Channel.Title != nil && *project.Channel.Title != "" {
|
||||||
|
name = *project.Channel.Title
|
||||||
|
} else if project.Channel.Username != nil && *project.Channel.Username != "" {
|
||||||
|
name = "@" + *project.Channel.Username
|
||||||
|
} else {
|
||||||
|
name = "Без названия"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем ссылку если есть username
|
||||||
|
if project.Channel.Username != nil && *project.Channel.Username != "" {
|
||||||
|
return fmt.Sprintf(" %s (@%s)\n", name, *project.Channel.Username)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(" %s\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCPF(cost float64, subscriptions int) float64 {
|
||||||
|
if subscriptions == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return cost / float64(subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateConversion(subscriptions int, views int) float64 {
|
||||||
|
if views == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(subscriptions) / float64(views) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatInviteLinkType(value string) string {
|
func formatInviteLinkType(value string) string {
|
||||||
|
|||||||
@@ -15,48 +15,52 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseOptionalDetails struct {
|
type PurchaseOptionalDetails struct {
|
||||||
ProjectID string
|
ProjectID string
|
||||||
ProjectTitle string
|
ProjectTitle string
|
||||||
CreativeID string
|
CreativeID string
|
||||||
CreativeTitle string
|
CreativeTitle string
|
||||||
Channels []PurchaseChannelInput
|
Channels []PurchaseChannelInput
|
||||||
PlacementDateTime *time.Time
|
PlacementDateTime *time.Time
|
||||||
PaymentDate *time.Time
|
PaymentDate *time.Time
|
||||||
CostType string
|
CostType string
|
||||||
CostValue *float64
|
CostValue *float64
|
||||||
CostBeforeType string
|
CostBeforeType string
|
||||||
CostBeforeBargain *CostEntry
|
CostBeforeBargain *CostEntry
|
||||||
PurchaseType string
|
PurchaseType string
|
||||||
Format string
|
Format string
|
||||||
Comment string
|
Comment string
|
||||||
InputMode string
|
InviteLinkType string
|
||||||
CurrentParam string
|
InputMode string
|
||||||
CurrentChannel string
|
CurrentParam string
|
||||||
ParamPage int
|
CurrentChannel string
|
||||||
ReturnMode string
|
ParamPage int
|
||||||
PlacementMode string
|
ReturnMode string
|
||||||
PaymentDateMode string
|
PlacementMode string
|
||||||
CostMode string
|
PaymentDateMode string
|
||||||
CostBeforeMode string
|
CostMode string
|
||||||
PurchaseTypeMode string
|
CostBeforeMode string
|
||||||
CommentMode string
|
PurchaseTypeMode string
|
||||||
FormatMode string
|
CommentMode string
|
||||||
PlacementByChannel map[string]*time.Time
|
FormatMode string
|
||||||
PaymentDateByChannel map[string]*time.Time
|
InviteLinkTypeMode string
|
||||||
CostByChannel map[string]CostEntry
|
PlacementByChannel map[string]*time.Time
|
||||||
CostBeforeByChannel map[string]CostEntry
|
PaymentDateByChannel map[string]*time.Time
|
||||||
PurchaseTypeByChannel map[string]string
|
CostByChannel map[string]CostEntry
|
||||||
CommentByChannel map[string]string
|
CostBeforeByChannel map[string]CostEntry
|
||||||
FormatByChannel map[string]string
|
PurchaseTypeByChannel map[string]string
|
||||||
PlacementCopy *time.Time
|
CommentByChannel map[string]string
|
||||||
PaymentDateCopy *time.Time
|
FormatByChannel map[string]string
|
||||||
CostCopy *CostEntry
|
InviteLinkTypeByChannel map[string]string
|
||||||
CostBeforeCopy *CostEntry
|
PlacementCopy *time.Time
|
||||||
PurchaseTypeCopy *string
|
PaymentDateCopy *time.Time
|
||||||
CommentCopy *string
|
CostCopy *CostEntry
|
||||||
FormatCopy *string
|
CostBeforeCopy *CostEntry
|
||||||
ChannelEditMode string // "edit" или "copy"
|
PurchaseTypeCopy *string
|
||||||
BackState bot.State
|
CommentCopy *string
|
||||||
|
FormatCopy *string
|
||||||
|
InviteLinkTypeCopy *string
|
||||||
|
ChannelEditMode string // "edit" или "copy"
|
||||||
|
BackState bot.State
|
||||||
}
|
}
|
||||||
|
|
||||||
type CostEntry struct {
|
type CostEntry struct {
|
||||||
@@ -104,6 +108,7 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
))
|
))
|
||||||
rows = append(rows, Row(
|
rows = append(rows, Row(
|
||||||
s.formatButton(),
|
s.formatButton(),
|
||||||
|
s.inviteLinkTypeButton(),
|
||||||
))
|
))
|
||||||
|
|
||||||
if row := s.costBeforeDeleteRow(); row != nil {
|
if row := s.costBeforeDeleteRow(); row != nil {
|
||||||
@@ -182,6 +187,16 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
|||||||
s.openCommonEditor(b, "format")
|
s.openCommonEditor(b, "format")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "opt_invite_link_type":
|
||||||
|
// Открываем редактор в текущем режиме
|
||||||
|
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||||||
|
s.InputMode = "invite_link_type_channels"
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
|
} else {
|
||||||
|
s.InputMode = "invite_link_type"
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
|
}
|
||||||
|
|
||||||
case "opt_comment":
|
case "opt_comment":
|
||||||
// Открываем редактор в текущем режиме
|
// Открываем редактор в текущем режиме
|
||||||
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
|
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
|
||||||
@@ -332,6 +347,19 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
|||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(u.CallbackQuery.Data, "invite_link_type:") {
|
||||||
|
value := strings.TrimPrefix(u.CallbackQuery.Data, "invite_link_type:")
|
||||||
|
s.setInviteLinkTypeValue(value)
|
||||||
|
if s.ReturnMode != "" {
|
||||||
|
s.InputMode = s.ReturnMode
|
||||||
|
s.ReturnMode = ""
|
||||||
|
} else {
|
||||||
|
s.InputMode = ""
|
||||||
|
}
|
||||||
|
s.CurrentChannel = ""
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,6 +461,9 @@ func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
|
|||||||
if s.hasFormatValue() {
|
if s.hasFormatValue() {
|
||||||
lines = append(lines, fmt.Sprintf("<i>Формат:</i> <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails()))
|
lines = append(lines, fmt.Sprintf("<i>Формат:</i> <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails()))
|
||||||
}
|
}
|
||||||
|
if s.hasInviteLinkTypeValue() {
|
||||||
|
lines = append(lines, fmt.Sprintf("<i>Тип ссылки:</i> <b>%s</b>%s", s.formatInviteLinkTypeSummary(), s.formatInviteLinkTypeDetails()))
|
||||||
|
}
|
||||||
if s.hasCommentValue() {
|
if s.hasCommentValue() {
|
||||||
lines = append(lines, fmt.Sprintf("<i>Комментарий:</i> <b>%s</b>%s", s.formatCommentSummary(), s.formatCommentDetails()))
|
lines = append(lines, fmt.Sprintf("<i>Комментарий:</i> <b>%s</b>%s", s.formatCommentSummary(), s.formatCommentDetails()))
|
||||||
}
|
}
|
||||||
@@ -449,8 +480,7 @@ func (s *PurchaseOptionalDetails) formatDateTime(value *time.Time) string {
|
|||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
local := value.In(ui2.MskLocation)
|
local := value.In(ui2.MskLocation)
|
||||||
text := fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
|
return fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
|
||||||
return fmt.Sprintf("<b>%s</b>", text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
|
func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
|
||||||
@@ -458,23 +488,21 @@ func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
|
|||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
local := value.In(ui2.MskLocation)
|
local := value.In(ui2.MskLocation)
|
||||||
text := fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
|
return fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
|
||||||
return fmt.Sprintf("<b>%s</b>", text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatText(value string) string {
|
func (s *PurchaseOptionalDetails) formatText(value string) string {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("<b>%s</b>", value)
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatCostValue() string {
|
func (s *PurchaseOptionalDetails) formatCostValue() string {
|
||||||
if s.CostValue == nil {
|
if s.CostValue == nil {
|
||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
text := fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
|
return fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
|
||||||
return fmt.Sprintf("<b>%s</b>", text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatCostBefore() string {
|
func (s *PurchaseOptionalDetails) formatCostBefore() string {
|
||||||
@@ -482,8 +510,7 @@ func (s *PurchaseOptionalDetails) formatCostBefore() string {
|
|||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain)
|
label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain)
|
||||||
text := fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
|
return fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
|
||||||
return fmt.Sprintf("<b>%s</b>", text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) costTypeLabel() string {
|
func (s *PurchaseOptionalDetails) costTypeLabel() string {
|
||||||
@@ -576,6 +603,22 @@ func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton
|
|||||||
return Button(fmt.Sprintf("%s Комментарий%s", icon, modeIcon), "opt_comment")
|
return Button(fmt.Sprintf("%s Комментарий%s", icon, modeIcon), "opt_comment")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseOptionalDetails) inviteLinkTypeButton() echotron.InlineKeyboardButton {
|
||||||
|
icon := "+"
|
||||||
|
if s.hasInviteLinkTypeValue() {
|
||||||
|
icon = "✎"
|
||||||
|
}
|
||||||
|
modeIcon := ""
|
||||||
|
if len(s.Channels) > 1 && s.InviteLinkTypeMode == "per_channel" {
|
||||||
|
modeIcon = " 👥"
|
||||||
|
}
|
||||||
|
label := "Тип ссылки"
|
||||||
|
if s.InviteLinkType != "" {
|
||||||
|
label += ": " + s.inviteLinkTypeLabel(s.InviteLinkType)
|
||||||
|
}
|
||||||
|
return Button(fmt.Sprintf("%s %s%s", icon, label, modeIcon), "opt_invite_link_type")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton {
|
func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton {
|
||||||
return Button("⚙️ Режимы параметров", "toggle_global_mode")
|
return Button("⚙️ Режимы параметров", "toggle_global_mode")
|
||||||
}
|
}
|
||||||
@@ -686,6 +729,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
|
|||||||
return
|
return
|
||||||
case "format_custom":
|
case "format_custom":
|
||||||
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
|
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
|
||||||
|
case "invite_link_type":
|
||||||
|
text = "<i>Страница выбора типа ссылки</i>\n\n"
|
||||||
|
if s.CurrentChannel != "" {
|
||||||
|
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||||||
|
}
|
||||||
|
text += "Выберите тип ссылки\n\n"
|
||||||
|
keyboard := Keyboard(
|
||||||
|
Row(
|
||||||
|
Button("Открытая", "invite_link_type:public"),
|
||||||
|
Button("С заявками", "invite_link_type:approval"),
|
||||||
|
),
|
||||||
|
Row(Button("← Назад", s.backAction())),
|
||||||
|
)
|
||||||
|
if mode == bot.EditMessage {
|
||||||
|
b.Edit(text, keyboard)
|
||||||
|
} else {
|
||||||
|
b.SendNew(text, keyboard)
|
||||||
|
}
|
||||||
|
return
|
||||||
case "comment":
|
case "comment":
|
||||||
text += "<b>Введите комментарий</b>"
|
text += "<b>Введите комментарий</b>"
|
||||||
default:
|
default:
|
||||||
@@ -761,6 +823,15 @@ func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMo
|
|||||||
}
|
}
|
||||||
rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format")))
|
rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format")))
|
||||||
|
|
||||||
|
// Тип ссылки
|
||||||
|
inviteLinkTypeLabel := "Тип ссылки: "
|
||||||
|
if s.InviteLinkTypeMode == "per_channel" {
|
||||||
|
inviteLinkTypeLabel += "👥"
|
||||||
|
} else {
|
||||||
|
inviteLinkTypeLabel += "общий"
|
||||||
|
}
|
||||||
|
rows = append(rows, Row(Button(inviteLinkTypeLabel, "toggle_param_mode:invite_link_type")))
|
||||||
|
|
||||||
rows = append(rows, Row(Button("← Назад", "back_to_optional")))
|
rows = append(rows, Row(Button("← Назад", "back_to_optional")))
|
||||||
|
|
||||||
b.Render(text, Keyboard(rows...), mode)
|
b.Render(text, Keyboard(rows...), mode)
|
||||||
@@ -789,6 +860,9 @@ func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.Render
|
|||||||
case "format_channels":
|
case "format_channels":
|
||||||
s.renderParamChannels(b, mode, "format")
|
s.renderParamChannels(b, mode, "format")
|
||||||
return true
|
return true
|
||||||
|
case "invite_link_type_channels":
|
||||||
|
s.renderParamChannels(b, mode, "invite_link_type")
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -928,7 +1002,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
|
|||||||
// Паддинг вычисляем так, чтобы все значения начинались с одной позиции
|
// Паддинг вычисляем так, чтобы все значения начинались с одной позиции
|
||||||
const (
|
const (
|
||||||
marker = "· " // Маркер пункта
|
marker = "· " // Маркер пункта
|
||||||
valueIndent = 2 // Отступ после самого длинного названия
|
valueIndent = 6 // Отступ после самого длинного названия (увеличен для лучшей читаемости)
|
||||||
)
|
)
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
@@ -936,7 +1010,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
|
|||||||
labelLen := len([]rune(line.label))
|
labelLen := len([]rune(line.label))
|
||||||
// Паддинг = (макс.длина - тек.длина) + отступ после названия
|
// Паддинг = (макс.длина - тек.длина) + отступ после названия
|
||||||
paddingLen := maxLabelLen - labelLen + valueIndent
|
paddingLen := maxLabelLen - labelLen + valueIndent
|
||||||
padding := strings.Repeat(" ", paddingLen)
|
padding := strings.Repeat("\u00A0", paddingLen)
|
||||||
|
|
||||||
formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value)
|
formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value)
|
||||||
result = append(result, formattedLine)
|
result = append(result, formattedLine)
|
||||||
@@ -953,6 +1027,9 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
|
|||||||
|
|
||||||
finalResult := strings.Join(result, "\n")
|
finalResult := strings.Join(result, "\n")
|
||||||
|
|
||||||
|
// Оборачиваем в <pre> для моноширинного шрифта (выравнивание работает только в monospace)
|
||||||
|
finalResult = fmt.Sprintf("<pre>%s</pre>", finalResult)
|
||||||
|
|
||||||
// Логируем итоговый результат
|
// Логируем итоговый результат
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("param", param).
|
Str("param", param).
|
||||||
@@ -1100,6 +1177,11 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
|
|||||||
s.ReturnMode = ""
|
s.ReturnMode = ""
|
||||||
s.CurrentChannel = ""
|
s.CurrentChannel = ""
|
||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
|
case "invite_link_type":
|
||||||
|
s.InputMode = "invite_link_type"
|
||||||
|
s.ReturnMode = ""
|
||||||
|
s.CurrentChannel = ""
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
default:
|
default:
|
||||||
s.InputMode = ""
|
s.InputMode = ""
|
||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
@@ -1151,6 +1233,11 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
|
|||||||
s.ReturnMode = "format_channels"
|
s.ReturnMode = "format_channels"
|
||||||
s.CurrentChannel = channelKey
|
s.CurrentChannel = channelKey
|
||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
|
case "invite_link_type":
|
||||||
|
s.InputMode = "invite_link_type"
|
||||||
|
s.ReturnMode = "invite_link_type_channels"
|
||||||
|
s.CurrentChannel = channelKey
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
default:
|
default:
|
||||||
s.InputMode = ""
|
s.InputMode = ""
|
||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
@@ -1204,6 +1291,13 @@ func (s *PurchaseOptionalDetails) copyParamValue(param, channelKey string) {
|
|||||||
} else {
|
} else {
|
||||||
s.FormatCopy = nil
|
s.FormatCopy = nil
|
||||||
}
|
}
|
||||||
|
case "invite_link_type":
|
||||||
|
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok {
|
||||||
|
copied := value
|
||||||
|
s.InviteLinkTypeCopy = &copied
|
||||||
|
} else {
|
||||||
|
s.InviteLinkTypeCopy = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,6 +1369,14 @@ func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
|
|||||||
key := channelKey(ch)
|
key := channelKey(ch)
|
||||||
s.FormatByChannel[key] = s.Format
|
s.FormatByChannel[key] = s.Format
|
||||||
}
|
}
|
||||||
|
case "invite_link_type":
|
||||||
|
if s.InviteLinkType == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, ch := range s.Channels {
|
||||||
|
key := channelKey(ch)
|
||||||
|
s.InviteLinkTypeByChannel[key] = s.InviteLinkType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1334,6 +1436,12 @@ func (s *PurchaseOptionalDetails) pasteParamValue(param, channelKey string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.FormatByChannel[channelKey] = *s.FormatCopy
|
s.FormatByChannel[channelKey] = *s.FormatCopy
|
||||||
|
case "invite_link_type":
|
||||||
|
if s.InviteLinkTypeCopy == nil {
|
||||||
|
delete(s.InviteLinkTypeByChannel, channelKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.InviteLinkTypeByChannel[channelKey] = *s.InviteLinkTypeCopy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1367,6 +1475,10 @@ func (s *PurchaseOptionalDetails) applyParamToAll(param string) {
|
|||||||
for _, ch := range s.Channels {
|
for _, ch := range s.Channels {
|
||||||
s.pasteParamValue(param, channelKey(ch))
|
s.pasteParamValue(param, channelKey(ch))
|
||||||
}
|
}
|
||||||
|
case "invite_link_type":
|
||||||
|
for _, ch := range s.Channels {
|
||||||
|
s.pasteParamValue(param, channelKey(ch))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1386,6 +1498,8 @@ func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
|
|||||||
delete(s.CommentByChannel, channelKey)
|
delete(s.CommentByChannel, channelKey)
|
||||||
case "format":
|
case "format":
|
||||||
delete(s.FormatByChannel, channelKey)
|
delete(s.FormatByChannel, channelKey)
|
||||||
|
case "invite_link_type":
|
||||||
|
delete(s.InviteLinkTypeByChannel, channelKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1405,6 +1519,8 @@ func (s *PurchaseOptionalDetails) paramTitle(param string) string {
|
|||||||
return "Комментарий"
|
return "Комментарий"
|
||||||
case "format":
|
case "format":
|
||||||
return "Формат"
|
return "Формат"
|
||||||
|
case "invite_link_type":
|
||||||
|
return "Тип ссылки"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -1447,6 +1563,11 @@ func (s *PurchaseOptionalDetails) paramMode(param string) string {
|
|||||||
return "common"
|
return "common"
|
||||||
}
|
}
|
||||||
return s.FormatMode
|
return s.FormatMode
|
||||||
|
case "invite_link_type":
|
||||||
|
if s.InviteLinkTypeMode == "" {
|
||||||
|
return "common"
|
||||||
|
}
|
||||||
|
return s.InviteLinkTypeMode
|
||||||
default:
|
default:
|
||||||
return "common"
|
return "common"
|
||||||
}
|
}
|
||||||
@@ -1468,6 +1589,8 @@ func (s *PurchaseOptionalDetails) setParamMode(param, mode string) {
|
|||||||
s.CommentMode = mode
|
s.CommentMode = mode
|
||||||
case "format":
|
case "format":
|
||||||
s.FormatMode = mode
|
s.FormatMode = mode
|
||||||
|
case "invite_link_type":
|
||||||
|
s.InviteLinkTypeMode = mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1493,6 +1616,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
|
|||||||
if s.FormatMode == "" {
|
if s.FormatMode == "" {
|
||||||
s.FormatMode = "common"
|
s.FormatMode = "common"
|
||||||
}
|
}
|
||||||
|
if s.InviteLinkTypeMode == "" {
|
||||||
|
s.InviteLinkTypeMode = "common"
|
||||||
|
}
|
||||||
if s.PlacementByChannel == nil {
|
if s.PlacementByChannel == nil {
|
||||||
s.PlacementByChannel = make(map[string]*time.Time)
|
s.PlacementByChannel = make(map[string]*time.Time)
|
||||||
}
|
}
|
||||||
@@ -1514,6 +1640,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
|
|||||||
if s.FormatByChannel == nil {
|
if s.FormatByChannel == nil {
|
||||||
s.FormatByChannel = make(map[string]string)
|
s.FormatByChannel = make(map[string]string)
|
||||||
}
|
}
|
||||||
|
if s.InviteLinkTypeByChannel == nil {
|
||||||
|
s.InviteLinkTypeByChannel = make(map[string]string)
|
||||||
|
}
|
||||||
s.syncChannelMaps()
|
s.syncChannelMaps()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1560,6 +1689,11 @@ func (s *PurchaseOptionalDetails) syncChannelMaps() {
|
|||||||
delete(s.FormatByChannel, key)
|
delete(s.FormatByChannel, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for key := range s.InviteLinkTypeByChannel {
|
||||||
|
if _, ok := valid[key]; !ok {
|
||||||
|
delete(s.InviteLinkTypeByChannel, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatPlacementSummary() string {
|
func (s *PurchaseOptionalDetails) formatPlacementSummary() string {
|
||||||
@@ -1573,7 +1707,7 @@ func (s *PurchaseOptionalDetails) formatPlacementDetails() string {
|
|||||||
if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("placement")
|
return s.renderParamChannelSummary("placement")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string {
|
func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string {
|
||||||
@@ -1587,7 +1721,7 @@ func (s *PurchaseOptionalDetails) formatPaymentDateDetails() string {
|
|||||||
if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("payment_date")
|
return s.renderParamChannelSummary("payment_date")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatCostSummary() string {
|
func (s *PurchaseOptionalDetails) formatCostSummary() string {
|
||||||
@@ -1601,7 +1735,7 @@ func (s *PurchaseOptionalDetails) formatCostDetails() string {
|
|||||||
if s.CostMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.CostMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("cost")
|
return s.renderParamChannelSummary("cost")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string {
|
func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string {
|
||||||
@@ -1615,7 +1749,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeDetails() string {
|
|||||||
if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("cost_before")
|
return s.renderParamChannelSummary("cost_before")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string {
|
func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string {
|
||||||
@@ -1629,7 +1763,7 @@ func (s *PurchaseOptionalDetails) formatPurchaseTypeDetails() string {
|
|||||||
if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("purchase_type")
|
return s.renderParamChannelSummary("purchase_type")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatCommentSummary() string {
|
func (s *PurchaseOptionalDetails) formatCommentSummary() string {
|
||||||
@@ -1643,7 +1777,7 @@ func (s *PurchaseOptionalDetails) formatCommentDetails() string {
|
|||||||
if s.CommentMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.CommentMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("comment")
|
return s.renderParamChannelSummary("comment")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatFormatSummary() string {
|
func (s *PurchaseOptionalDetails) formatFormatSummary() string {
|
||||||
@@ -1657,7 +1791,21 @@ func (s *PurchaseOptionalDetails) formatFormatDetails() string {
|
|||||||
if s.FormatMode != "per_channel" || len(s.Channels) <= 1 {
|
if s.FormatMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "\n" + s.renderParamChannelSummary("format")
|
return s.renderParamChannelSummary("format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseOptionalDetails) formatInviteLinkTypeSummary() string {
|
||||||
|
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||||||
|
return "👥"
|
||||||
|
}
|
||||||
|
return s.inviteLinkTypeLabel(s.InviteLinkType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseOptionalDetails) formatInviteLinkTypeDetails() string {
|
||||||
|
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.renderParamChannelSummary("invite_link_type")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) string {
|
func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) string {
|
||||||
@@ -1674,17 +1822,22 @@ func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) str
|
|||||||
return s.formatCostBeforeForChannel(channelKey)
|
return s.formatCostBeforeForChannel(channelKey)
|
||||||
case "purchase_type":
|
case "purchase_type":
|
||||||
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
|
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
|
||||||
return fmt.Sprintf("<b>%s</b>", value)
|
return value
|
||||||
}
|
}
|
||||||
return "—"
|
return "—"
|
||||||
case "comment":
|
case "comment":
|
||||||
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
|
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
|
||||||
return fmt.Sprintf("<b>%s</b>", value)
|
return value
|
||||||
}
|
}
|
||||||
return "—"
|
return "—"
|
||||||
case "format":
|
case "format":
|
||||||
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
|
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
|
||||||
return fmt.Sprintf("<b>%s</b>", value)
|
return value
|
||||||
|
}
|
||||||
|
return "—"
|
||||||
|
case "invite_link_type":
|
||||||
|
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
|
||||||
|
return s.inviteLinkTypeLabel(value)
|
||||||
}
|
}
|
||||||
return "—"
|
return "—"
|
||||||
default:
|
default:
|
||||||
@@ -1698,8 +1851,7 @@ func (s *PurchaseOptionalDetails) formatCostForChannel(channelKey string) string
|
|||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
label := s.costTypeLabelForEntry(entry)
|
label := s.costTypeLabelForEntry(entry)
|
||||||
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value)
|
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
|
||||||
return fmt.Sprintf("<b>%s</b>", text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string {
|
func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string {
|
||||||
@@ -1708,8 +1860,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string)
|
|||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
label := s.costBeforeTypeLabelForEntry(entry)
|
label := s.costBeforeTypeLabelForEntry(entry)
|
||||||
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value)
|
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
|
||||||
return fmt.Sprintf("<b>%s</b>", text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
|
func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
|
||||||
@@ -1728,6 +1879,8 @@ func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
|
|||||||
return s.CommentCopy != nil
|
return s.CommentCopy != nil
|
||||||
case "format":
|
case "format":
|
||||||
return s.FormatCopy != nil
|
return s.FormatCopy != nil
|
||||||
|
case "invite_link_type":
|
||||||
|
return s.InviteLinkTypeCopy != nil
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1756,6 +1909,9 @@ func (s *PurchaseOptionalDetails) hasChannelValue(param, channelKey string) bool
|
|||||||
case "format":
|
case "format":
|
||||||
value, ok := s.FormatByChannel[channelKey]
|
value, ok := s.FormatByChannel[channelKey]
|
||||||
return ok && value != ""
|
return ok && value != ""
|
||||||
|
case "invite_link_type":
|
||||||
|
value, ok := s.InviteLinkTypeByChannel[channelKey]
|
||||||
|
return ok && value != ""
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1845,6 +2001,28 @@ func (s *PurchaseOptionalDetails) hasCommentValue() bool {
|
|||||||
return s.Comment != ""
|
return s.Comment != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseOptionalDetails) hasInviteLinkTypeValue() bool {
|
||||||
|
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
|
||||||
|
for _, value := range s.InviteLinkTypeByChannel {
|
||||||
|
if value != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.InviteLinkType != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseOptionalDetails) inviteLinkTypeLabel(linkType string) string {
|
||||||
|
if linkType == "public" {
|
||||||
|
return "Открытая"
|
||||||
|
}
|
||||||
|
if linkType == "approval" {
|
||||||
|
return "С заявками"
|
||||||
|
}
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) costTypeLabelForEntry(entry CostEntry) string {
|
func (s *PurchaseOptionalDetails) costTypeLabelForEntry(entry CostEntry) string {
|
||||||
if entry.Type == "cpm" {
|
if entry.Type == "cpm" {
|
||||||
return "СРМ"
|
return "СРМ"
|
||||||
@@ -1966,6 +2144,14 @@ func (s *PurchaseOptionalDetails) setFormatValue(value string) {
|
|||||||
s.FormatByChannel[s.CurrentChannel] = value
|
s.FormatByChannel[s.CurrentChannel] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseOptionalDetails) setInviteLinkTypeValue(value string) {
|
||||||
|
if s.CurrentChannel == "" {
|
||||||
|
s.InviteLinkType = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.InviteLinkTypeByChannel[s.CurrentChannel] = value
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) backAction() string {
|
func (s *PurchaseOptionalDetails) backAction() string {
|
||||||
if s.ReturnMode != "" {
|
if s.ReturnMode != "" {
|
||||||
return "back_to_return"
|
return "back_to_return"
|
||||||
@@ -2081,6 +2267,11 @@ func (s *PurchaseOptionalDetails) buildPlacementDetails(placementType *string) *
|
|||||||
details.Format = &s.Format
|
details.Format = &s.Format
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
|
||||||
|
if s.InviteLinkType != "" {
|
||||||
|
details.InviteLinkType = &s.InviteLinkType
|
||||||
|
}
|
||||||
|
}
|
||||||
if placementType != nil && (s.PurchaseTypeMode == "per_channel" || len(s.Channels) <= 1) {
|
if placementType != nil && (s.PurchaseTypeMode == "per_channel" || len(s.Channels) <= 1) {
|
||||||
details.PlacementType = placementType
|
details.PlacementType = placementType
|
||||||
}
|
}
|
||||||
@@ -2130,6 +2321,11 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string) *backen
|
|||||||
details.Format = &value
|
details.Format = &value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.InviteLinkTypeMode == "per_channel" {
|
||||||
|
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
|
||||||
|
details.InviteLinkType = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user