Build notification template registry from backend code

This commit is contained in:
Ruslan Bakiev
2026-04-06 15:04:45 +07:00
parent ac5ee256fd
commit befec16a84
8 changed files with 142 additions and 362 deletions

View File

@@ -444,6 +444,23 @@ export type NotificationHistoryItem = {
title: Scalars['String']['output']; title: Scalars['String']['output'];
}; };
export type NotificationTemplate = {
__typename?: 'NotificationTemplate';
channels: Array<NotificationTemplateChannel>;
id: Scalars['ID']['output'];
title: Scalars['String']['output'];
};
export type NotificationTemplateChannel = {
__typename?: 'NotificationTemplateChannel';
body: Array<Scalars['String']['output']>;
buttonText?: Maybe<Scalars['String']['output']>;
buttonUrl?: Maybe<Scalars['String']['output']>;
channel: LoginChannel;
implemented: Scalars['Boolean']['output'];
subject?: Maybe<Scalars['String']['output']>;
};
export type Order = { export type Order = {
__typename?: 'Order'; __typename?: 'Order';
blockReason?: Maybe<Scalars['String']['output']>; blockReason?: Maybe<Scalars['String']['output']>;
@@ -549,6 +566,7 @@ export type Query = {
myMessengerConnections: Array<MessengerConnection>; myMessengerConnections: Array<MessengerConnection>;
myNotificationHistory: Array<NotificationHistoryItem>; myNotificationHistory: Array<NotificationHistoryItem>;
myOrders: Array<Order>; myOrders: Array<Order>;
notificationTemplates: Array<NotificationTemplate>;
order?: Maybe<Order>; order?: Maybe<Order>;
referralStats: ReferralStats; referralStats: ReferralStats;
registrationRequests: Array<RegistrationRequest>; registrationRequests: Array<RegistrationRequest>;
@@ -959,6 +977,11 @@ export type MyNotificationHistoryQueryVariables = Exact<{
export type MyNotificationHistoryQuery = { __typename?: 'Query', myNotificationHistory: Array<{ __typename?: 'NotificationHistoryItem', id: string, channel: MessengerType, title: string, message: string, createdAt: any, orderId?: string | null }> }; export type MyNotificationHistoryQuery = { __typename?: 'Query', myNotificationHistory: Array<{ __typename?: 'NotificationHistoryItem', id: string, channel: MessengerType, title: string, message: string, createdAt: any, orderId?: string | null }> };
export type NotificationTemplatesQueryVariables = Exact<{ [key: string]: never; }>;
export type NotificationTemplatesQuery = { __typename?: 'Query', notificationTemplates: Array<{ __typename?: 'NotificationTemplate', id: string, title: string, channels: Array<{ __typename?: 'NotificationTemplateChannel', channel: LoginChannel, implemented: boolean, subject?: string | null, body: Array<string>, buttonText?: string | null, buttonUrl?: string | null }> }> };
export type SendTestMessengerMessageMutationVariables = Exact<{ export type SendTestMessengerMessageMutationVariables = Exact<{
type: MessengerType; type: MessengerType;
channelId?: InputMaybe<Scalars['String']['input']>; channelId?: InputMaybe<Scalars['String']['input']>;
@@ -2273,6 +2296,42 @@ export function useMyNotificationHistoryLazyQuery(variables?: MyNotificationHist
return VueApolloComposable.useLazyQuery<MyNotificationHistoryQuery, MyNotificationHistoryQueryVariables>(MyNotificationHistoryDocument, variables, options); return VueApolloComposable.useLazyQuery<MyNotificationHistoryQuery, MyNotificationHistoryQueryVariables>(MyNotificationHistoryDocument, variables, options);
} }
export type MyNotificationHistoryQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<MyNotificationHistoryQuery, MyNotificationHistoryQueryVariables>; export type MyNotificationHistoryQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<MyNotificationHistoryQuery, MyNotificationHistoryQueryVariables>;
export const NotificationTemplatesDocument = gql`
query NotificationTemplates {
notificationTemplates {
id
title
channels {
channel
implemented
subject
body
buttonText
buttonUrl
}
}
}
`;
/**
* __useNotificationTemplatesQuery__
*
* To run a query within a Vue component, call `useNotificationTemplatesQuery` and pass it any options that fit your needs.
* When your component renders, `useNotificationTemplatesQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
*
* @example
* const { result, loading, error } = useNotificationTemplatesQuery();
*/
export function useNotificationTemplatesQuery(options: VueApolloComposable.UseQueryOptions<NotificationTemplatesQuery, NotificationTemplatesQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>> = {}) {
return VueApolloComposable.useQuery<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>(NotificationTemplatesDocument, {}, options);
}
export function useNotificationTemplatesLazyQuery(options: VueApolloComposable.UseQueryOptions<NotificationTemplatesQuery, NotificationTemplatesQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>(NotificationTemplatesDocument, {}, options);
}
export type NotificationTemplatesQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<NotificationTemplatesQuery, NotificationTemplatesQueryVariables>;
export const SendTestMessengerMessageDocument = gql` export const SendTestMessengerMessageDocument = gql`
mutation SendTestMessengerMessage($type: MessengerType!, $channelId: String, $message: String) { mutation SendTestMessengerMessage($type: MessengerType!, $channelId: String, $message: String) {
sendTestMessengerMessage(type: $type, channelId: $channelId, message: $message) { sendTestMessengerMessage(type: $type, channelId: $channelId, message: $message) {

View File

@@ -247,13 +247,13 @@ function formatAmount(value: number) {
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/bonus-program?entry=manager-preview" to="/messages"
class="surface-card surface-card-interactive rounded-3xl bg-[#0d0d0f] p-5 text-white" class="surface-card surface-card-interactive rounded-3xl p-5"
> >
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/55">Client view</p> <p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Шаблоны</p>
<h2 class="mt-3 text-xl font-black tracking-[-0.03em] text-white">Открыть бонусный экран клиента</h2> <h2 class="mt-3 text-xl font-black tracking-[-0.03em] text-[#123824]">Открыть реестр уведомлений</h2>
<p class="mt-2 text-sm leading-6 text-white/72"> <p class="mt-2 text-sm leading-6 text-[#557562]">
Посмотреть, куда ведёт бонусное уведомление и как выглядит отдельный интерфейс программы. Посмотреть реальные шаблоны из backend-кода и быстро понять, что именно мы отправляем клиенту.
</p> </p>
</NuxtLink> </NuxtLink>
</div> </div>

View File

@@ -1,227 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'; import { useQuery } from '@vue/apollo-composable';
import { import {
MeDocument, NotificationTemplatesDocument,
MyMessengerConnectionsDocument, type NotificationTemplatesQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
type DeliveryChannel = 'EMAIL' | 'TELEGRAM' | 'MAX'; definePageMeta({
middleware: ['manager-only'],
type MessagePreview = {
channel: DeliveryChannel;
mode: 'live' | 'preview';
subject?: string;
body: string[];
buttonLabel?: string;
buttonTo?: string;
note: string;
};
type MessageScenario = {
id: string;
title: string;
trigger: string;
description: string;
previews: MessagePreview[];
};
const meQuery = useQuery(MeDocument);
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
const me = computed(() => meQuery.result.value?.me ?? null);
const channelState = computed<Record<DeliveryChannel, string>>(() => {
const connections = connectionsQuery.result.value?.myMessengerConnections ?? [];
const telegramConnected = connections.some((item) => item.type === 'TELEGRAM' && item.isActive);
const maxConnected = connections.some((item) => item.type === 'MAX' && item.isActive);
return {
EMAIL: me.value?.email ? `Email ${me.value.email}` : 'Email не определён',
TELEGRAM: telegramConnected ? 'Telegram подключён' : 'Telegram ещё не подключён',
MAX: maxConnected ? 'Max подключён' : 'Max ещё не подключён',
};
}); });
const messageScenarios = computed<MessageScenario[]>(() => { type TemplateItem = NotificationTemplatesQuery['notificationTemplates'][number];
const companyGreeting = me.value?.fullName || 'Клиент Фрегат'; type TemplateChannel = TemplateItem['channels'][number];
return [ const templatesQuery = useQuery(NotificationTemplatesDocument);
{
id: 'order-offer',
title: 'Публикация расчёта по заказу',
trigger: 'Менеджер заполнил стоимость, логистику и опубликовал предложение клиенту.',
description: 'Этот сценарий нужен для согласования финального текста по заказам и кнопки перехода в карточку заказа.',
previews: [
{
channel: 'EMAIL',
mode: 'preview',
subject: 'Fregat: предложение по заказу готово',
body: [
`Здравствуйте, ${companyGreeting}.`,
'По вашему заказу менеджер подготовил предложение: стоимость, логистика и условия уже доступны в личном кабинете.',
'Откройте карточку заказа, чтобы проверить детали и подтвердить запуск.',
],
buttonLabel: 'Открыть заказ',
buttonTo: '/orders',
note: 'Email-версия пока как локальный шаблон для согласования.',
},
{
channel: 'TELEGRAM',
mode: 'live',
body: [
'Заказ FRG-2401 изменил статус: WAITING_DOUBLE_CONFIRM.',
'Комментарий: менеджер опубликовал предложение и готов к запуску.',
],
buttonLabel: 'Открыть заказ',
buttonTo: '/orders',
note: 'Messenger-кнопка уже соответствует текущей логике перехода в заказ.',
},
{
channel: 'MAX',
mode: 'live',
body: [
'Заказ FRG-2401 изменил статус: WAITING_DOUBLE_CONFIRM.',
'Комментарий: менеджер опубликовал предложение и готов к запуску.',
],
buttonLabel: 'Открыть заказ',
buttonTo: '/orders',
note: 'Для Max показываем тот же сценарий с общей кнопкой открытия заказа.',
},
],
},
{
id: 'order-tracking',
title: 'Трекинг-обновление по заказу',
trigger: 'По заказу пришёл новый статус исполнения, доставки или производства.',
description: 'Здесь мы согласуем формат сообщения, когда заказ уже пошёл в работу и клиенту важен быстрый переход в детали.',
previews: [
{
channel: 'EMAIL',
mode: 'preview',
subject: 'Fregat: статус заказа обновлён',
body: [
`Здравствуйте, ${companyGreeting}.`,
'По заказу FRG-2401 пришло новое обновление: производство в работе, следующая контрольная точка - отгрузка.',
'Откройте заказ, чтобы увидеть логистику и актуальные параметры.',
],
buttonLabel: 'Перейти к заказу',
buttonTo: '/orders',
note: 'Почтовая версия нужна как отдельный шаблон поверх messenger-каналов.',
},
{
channel: 'TELEGRAM',
mode: 'live',
body: [
'Заказ FRG-2401 изменил статус: IN_PROGRESS.',
'Комментарий: заказ передан в производство и движется по плану.',
],
buttonLabel: 'Открыть заказ',
buttonTo: '/orders',
note: 'Это текущий реальный паттерн для order-status уведомлений.',
},
{
channel: 'MAX',
mode: 'live',
body: [
'Заказ FRG-2401 изменил статус: IN_PROGRESS.',
'Комментарий: заказ передан в производство и движется по плану.',
],
buttonLabel: 'Открыть заказ',
buttonTo: '/orders',
note: 'Max получает идентичную структуру, чтобы каналы были синхронизированы.',
},
],
},
{
id: 'bonus-balance',
title: 'Изменение бонусного баланса',
trigger: 'Клиенту начислили реферальный бонус или ручную бонусную транзакцию.',
description: 'Для бонусной программы делаем отдельный визуальный мир. Клик из уведомления должен уводить не в обычный кабинет, а в бонусный интерфейс.',
previews: [
{
channel: 'EMAIL',
mode: 'preview',
subject: 'Fregat Bonus: баланс обновлён',
body: [
`Здравствуйте, ${companyGreeting}.`,
'Ваш бонусный баланс изменился. Мы подготовили отдельный экран бонусной программы, чтобы вы видели начисления, выводы и историю в одном месте.',
'Откройте бонусный интерфейс и проверьте текущее состояние счёта.',
],
buttonLabel: 'Открыть бонусную программу',
buttonTo: '/bonus-program?entry=email-balance',
note: 'Email-сообщение пока как согласуемый шаблон, но маршрут уже готов.',
},
{
channel: 'TELEGRAM',
mode: 'live',
body: [
'Начислен бонус: 1250. Причина: реферальное начисление за заказ FRG-2401.',
'Кнопка ниже открывает отдельный бонусный экран, а не обычный профиль.',
],
buttonLabel: 'Открыть бонусную программу',
buttonTo: '/bonus-program?entry=telegram-balance',
note: 'Эту же точку входа теперь можно использовать и в реальном Telegram-уведомлении.',
},
{
channel: 'MAX',
mode: 'live',
body: [
'Начислен бонус: 1250. Причина: реферальное начисление за заказ FRG-2401.',
'Кнопка ниже открывает отдельный бонусный экран, а не обычный профиль.',
],
buttonLabel: 'Открыть бонусную программу',
buttonTo: '/bonus-program?entry=max-balance',
note: 'Max ведёт в тот же отдельный бонусный интерфейс.',
},
],
},
{
id: 'bonus-withdrawal',
title: 'Решение по заявке на вывод',
trigger: 'Менеджер проверил заявку на вывод бонусов и оставил решение.',
description: 'Здесь важен единый UX: человек получает решение в мессенджере и сразу попадает в бонусный экран, где видит историю и статус.',
previews: [
{
channel: 'EMAIL',
mode: 'preview',
subject: 'Fregat Bonus: заявка на вывод обновлена',
body: [
`Здравствуйте, ${companyGreeting}.`,
'По вашей заявке на вывод бонусов появилось новое решение. Внутри бонусной программы уже отображён актуальный статус и комментарий менеджера.',
'Откройте бонусный кабинет, чтобы посмотреть результат.',
],
buttonLabel: 'Проверить вывод',
buttonTo: '/bonus-program?entry=email-withdrawal',
note: 'Почтовый шаблон пока служит для визуального согласования.',
},
{
channel: 'TELEGRAM',
mode: 'live',
body: [
'Заявка на вывод вознаграждения обновлена: APPROVED.',
'Комментарий: выплата подтверждена и передана в обработку.',
],
buttonLabel: 'Проверить бонусную программу',
buttonTo: '/bonus-program?entry=telegram-withdrawal',
note: 'Теперь такую кнопку можно привязать к реальному уведомлению о выводе.',
},
{
channel: 'MAX',
mode: 'live',
body: [
'Заявка на вывод вознаграждения обновлена: APPROVED.',
'Комментарий: выплата подтверждена и передана в обработку.',
],
buttonLabel: 'Проверить бонусную программу',
buttonTo: '/bonus-program?entry=max-withdrawal',
note: 'Маршрут единый: отдельный бонусный экран на этом же домене.',
},
],
},
];
});
function channelLabel(channel: DeliveryChannel) { const templates = computed<TemplateItem[]>(() => templatesQuery.result.value?.notificationTemplates ?? []);
function channelLabel(channel: TemplateChannel['channel']) {
if (channel === 'EMAIL') { if (channel === 'EMAIL') {
return 'Email'; return 'Email';
} }
@@ -230,129 +25,73 @@ function channelLabel(channel: DeliveryChannel) {
} }
return 'Max'; return 'Max';
} }
function channelBadgeClass(channel: DeliveryChannel) {
if (channel === 'EMAIL') {
return 'bg-[#f3f4f6] text-[#111827]';
}
if (channel === 'TELEGRAM') {
return 'bg-[#dff3ff] text-[#0f5d92]';
}
return 'bg-[#edf0ff] text-[#3a46a4]';
}
function modeLabel(mode: MessagePreview['mode']) {
return mode === 'live' ? 'Live route' : 'Preview';
}
function modeClass(mode: MessagePreview['mode']) {
return mode === 'live'
? 'bg-[#def7e8] text-[#0d854a]'
: 'bg-[#fff1d7] text-[#9a6100]';
}
</script> </script>
<template> <template>
<section class="space-y-6"> <section class="space-y-6">
<div class="manager-hero"> <div class="manager-hero">
<p class="manager-eyebrow">Message Board</p> <h1 class="manager-title">Реестр шаблонов уведомлений</h1>
<h1 class="manager-title">Локальная витрина уведомлений</h1>
<p class="manager-copy"> <p class="manager-copy">
Здесь собраны шаблоны сообщений для email, Telegram и Max: сами тексты, кнопки и точки входа. Экран собирается из backend-кода. Здесь только реальные шаблоны и реальные типы взаимодействия с клиентом,
Это удобная доска, чтобы быстро согласовать контент до полной привязки к продовым событиям. которые сейчас описаны в системе.
</p> </p>
</div> </div>
<div class="surface-card rounded-3xl p-5"> <div v-if="templatesQuery.loading.value" class="manager-empty-state">
<div class="flex flex-wrap gap-3 text-sm"> Загружаем шаблоны...
<span
v-for="(state, channel) in channelState"
:key="channel"
class="manager-channel-chip"
>
<span class="manager-channel-dot" :class="channel === 'EMAIL' ? 'bg-[#111827]' : channel === 'TELEGRAM' ? 'bg-[#229ed9]' : 'bg-[#2b7fff]'">
{{ channel === 'EMAIL' ? 'EM' : channel === 'TELEGRAM' ? 'TG' : 'MX' }}
</span>
<span>{{ state }}</span>
</span>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<NuxtLink to="/bonus-program?entry=message-board" class="btn border-0 bg-[#111827] text-white hover:bg-[#000]">
Открыть бонусный экран
</NuxtLink>
<NuxtLink to="/notifications" class="btn btn-outline border-[#d7e9de] bg-white">
Вернуться к уведомлениям
</NuxtLink>
</div>
</div> </div>
<div class="space-y-5"> <div v-else-if="templates.length === 0" class="manager-empty-state">
Шаблонов пока нет.
</div>
<div v-else class="space-y-4">
<article <article
v-for="scenario in messageScenarios" v-for="template in templates"
:key="scenario.id" :key="template.id"
class="surface-card rounded-3xl p-5" class="surface-card rounded-3xl p-5"
> >
<div class="space-y-2"> <h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">
<div class="flex flex-wrap items-start justify-between gap-3"> {{ template.title }}
<div> </h2>
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">{{ scenario.title }}</h2>
<p class="mt-1 text-sm font-semibold text-[#355947]">{{ scenario.trigger }}</p>
</div>
<span class="rounded-full bg-[#eef7f1] px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-[#0d854a]">
{{ scenario.previews.length }} канала
</span>
</div>
<p class="max-w-3xl text-sm leading-6 text-[#557562]">{{ scenario.description }}</p>
</div>
<div class="mt-5 grid gap-4 xl:grid-cols-3"> <div class="mt-4 grid gap-4 xl:grid-cols-3">
<section <section
v-for="preview in scenario.previews" v-for="channel in template.channels"
:key="`${scenario.id}-${preview.channel}`" :key="`${template.id}-${channel.channel}`"
class="rounded-[28px] border border-[#deebe4] bg-[#fbfdfb] p-4" class="rounded-[24px] border border-[#deebe4] bg-[#fbfdfb] p-4"
> >
<div class="flex flex-wrap items-center gap-2"> <h3 class="text-sm font-extrabold uppercase tracking-[0.14em] text-[#355947]">
<span class="rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em]" :class="channelBadgeClass(preview.channel)"> {{ channelLabel(channel.channel) }}
{{ channelLabel(preview.channel) }} </h3>
</span>
<span class="rounded-full px-3 py-1 text-xs font-semibold" :class="modeClass(preview.mode)">
{{ modeLabel(preview.mode) }}
</span>
</div>
<div class="mt-4 rounded-[24px] bg-white p-4 shadow-[0_14px_34px_rgba(18,56,36,0.06)]"> <div class="mt-3 rounded-[20px] bg-white p-4">
<p v-if="preview.subject" class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]"> <p
{{ preview.subject }} v-if="channel.subject"
class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]"
>
{{ channel.subject }}
</p> </p>
<div class="mt-3 space-y-3 text-sm leading-6 text-[#123824]"> <div class="mt-3 space-y-3 text-sm leading-6 text-[#123824]">
<p <p
v-for="line in preview.body" v-for="line in channel.body"
:key="line" :key="line"
:class="channel.implemented ? '' : 'text-[#6f8577]'"
> >
{{ line }} {{ line }}
</p> </p>
</div> </div>
<div v-if="preview.buttonLabel" class="mt-4"> <div v-if="channel.buttonText" class="mt-4 space-y-1 text-sm leading-6 text-[#355947]">
<NuxtLink <p class="font-semibold text-[#123824]">
v-if="preview.buttonTo" {{ channel.buttonText }}
:to="preview.buttonTo" </p>
class="btn h-11 rounded-full border-0 bg-[#139957] px-5 text-white hover:bg-[#0d854a]" <p v-if="channel.buttonUrl" class="break-all text-xs text-[#5c7b69]">
> {{ channel.buttonUrl }}
{{ preview.buttonLabel }} </p>
</NuxtLink>
<span
v-else
class="inline-flex rounded-full bg-[#139957] px-5 py-3 text-sm font-semibold text-white"
>
{{ preview.buttonLabel }}
</span>
</div> </div>
</div> </div>
<p class="mt-4 text-sm leading-6 text-[#557562]">{{ preview.note }}</p>
</section> </section>
</div> </div>
</article> </article>

View File

@@ -118,27 +118,6 @@ async function sendTest() {
</div> </div>
<div class="surface-card rounded-3xl p-5"> <div class="surface-card rounded-3xl p-5">
<div class="rounded-3xl border border-[#d6ebde] bg-[#f8fbf9] p-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="space-y-1">
<h2 class="text-xl font-bold text-[#123824]">Message board и бонусный экран</h2>
<p class="text-sm leading-6 text-[#557562]">
Для бонусных изменений теперь можно согласовывать отдельные сообщения и отдельную точку входа:
клик из уведомления открывает специальный бонусный интерфейс, а не обычный профиль.
</p>
</div>
<div class="flex flex-wrap gap-3">
<NuxtLink to="/messages" class="btn border-0 bg-[#111827] text-white hover:bg-[#000000]">
Открыть message board
</NuxtLink>
<NuxtLink to="/bonus-program?entry=notifications-preview" class="btn btn-outline border-[#d7e9de] bg-white">
Открыть бонусный экран
</NuxtLink>
</div>
</div>
</div>
<div class="mt-5"> <div class="mt-5">
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-xl font-bold text-[#123824]">Подключение каналов</h2> <h2 class="text-xl font-bold text-[#123824]">Подключение каналов</h2>

View File

@@ -139,18 +139,6 @@ const defaultDeliveryAddress = computed(() => deliveryAddresses.value.find((item
}} }}
</p> </p>
</NuxtLink> </NuxtLink>
<NuxtLink to="/bonus-program?entry=profile" class="block rounded-3xl bg-[#0d0d0f] p-5 text-white transition hover:shadow-[0_20px_48px_rgba(8,8,10,0.24)]">
<div class="mb-2 flex items-center justify-between gap-2">
<p class="text-lg font-bold">Бонусная программа</p>
<span class="rounded-full border border-white/10 bg-white/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-white/75">
Separate UI
</span>
</div>
<p class="text-sm leading-6 text-white/72">
Отдельный чёрный экран для бонусов, начислений и заявок на вывод. Именно туда можно вести пользователя из бонусных уведомлений.
</p>
</NuxtLink>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -75,21 +75,6 @@ async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
Подключите Telegram и Max, чтобы получать статусы заказов и важные уведомления в удобном канале. Подключите Telegram и Max, чтобы получать статусы заказов и важные уведомления в удобном канале.
</p> </p>
<div class="mt-4 rounded-3xl border border-[#d6ebde] bg-[#f8fbf9] p-4">
<p class="text-sm font-semibold text-[#123824]">Бонусная программа теперь может жить в отдельном экране</p>
<p class="mt-2 text-sm leading-6 text-[#557562]">
Для бонусных начислений и выводов можно отправлять отдельное сообщение с кнопкой, которое ведёт в специальный бонусный интерфейс.
</p>
<div class="mt-3 flex flex-wrap gap-3">
<NuxtLink to="/messages" class="btn border-0 bg-[#111827] text-white hover:bg-[#000000]">
Message board
</NuxtLink>
<NuxtLink to="/bonus-program?entry=profile-notifications" class="btn btn-outline border-[#d7e9de] bg-white">
Открыть бонусный экран
</NuxtLink>
</div>
</div>
<div class="mt-4 space-y-3"> <div class="mt-4 space-y-3">
<div class="rounded-2xl bg-[#f8fbf9] p-4 transition hover:shadow-md"> <div class="rounded-2xl bg-[#f8fbf9] p-4 transition hover:shadow-md">
<p class="font-semibold">Telegram</p> <p class="font-semibold">Telegram</p>

View File

@@ -0,0 +1,14 @@
query NotificationTemplates {
notificationTemplates {
id
title
channels {
channel
implemented
subject
body
buttonText
buttonUrl
}
}
}

View File

@@ -178,6 +178,21 @@ type NotificationHistoryItem {
orderId: ID orderId: ID
} }
type NotificationTemplateChannel {
channel: LoginChannel!
implemented: Boolean!
subject: String
body: [String!]!
buttonText: String
buttonUrl: String
}
type NotificationTemplate {
id: ID!
title: String!
channels: [NotificationTemplateChannel!]!
}
type Warehouse { type Warehouse {
id: ID! id: ID!
code: String! code: String!
@@ -364,6 +379,7 @@ type Query {
myDeliveryAddresses: [DeliveryAddress!]! myDeliveryAddresses: [DeliveryAddress!]!
myMessengerConnections: [MessengerConnection!]! myMessengerConnections: [MessengerConnection!]!
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]! myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
notificationTemplates: [NotificationTemplate!]!
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]! managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
clientProducts: [Product!]! clientProducts: [Product!]!
order(id: ID!): Order order(id: ID!): Order