feat(notifications): switch messenger connect flow to bot links

This commit is contained in:
Ruslan Bakiev
2026-04-02 15:58:09 +07:00
parent b01b01a360
commit 79100d05f8
6 changed files with 159 additions and 16 deletions

View File

@@ -523,6 +523,11 @@ export type ConsumeLoginTokenMutationVariables = Exact<{
export type ConsumeLoginTokenMutation = { __typename?: 'Mutation', consumeLoginToken: { __typename?: 'AuthSession', accessToken: string, expiresAt: any, user: { __typename?: 'User', id: string, email: string, fullName: string, role: UserRole, company?: { __typename?: 'Company', id: string } | null } } }; export type ConsumeLoginTokenMutation = { __typename?: 'Mutation', consumeLoginToken: { __typename?: 'AuthSession', accessToken: string, expiresAt: any, user: { __typename?: 'User', id: string, email: string, fullName: string, role: UserRole, company?: { __typename?: 'Company', id: string } | null } } };
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string, email: string } | null };
export type RegisterSelfMutationVariables = Exact<{ export type RegisterSelfMutationVariables = Exact<{
input: RegisterSelfInput; input: RegisterSelfInput;
}>; }>;
@@ -650,6 +655,34 @@ export function useConsumeLoginTokenMutation(options: VueApolloComposable.UseMut
return VueApolloComposable.useMutation<ConsumeLoginTokenMutation, ConsumeLoginTokenMutationVariables>(ConsumeLoginTokenDocument, options); return VueApolloComposable.useMutation<ConsumeLoginTokenMutation, ConsumeLoginTokenMutationVariables>(ConsumeLoginTokenDocument, options);
} }
export type ConsumeLoginTokenMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ConsumeLoginTokenMutation, ConsumeLoginTokenMutationVariables>; export type ConsumeLoginTokenMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ConsumeLoginTokenMutation, ConsumeLoginTokenMutationVariables>;
export const MeDocument = gql`
query Me {
me {
id
email
}
}
`;
/**
* __useMeQuery__
*
* To run a query within a Vue component, call `useMeQuery` and pass it any options that fit your needs.
* When your component renders, `useMeQuery` 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 } = useMeQuery();
*/
export function useMeQuery(options: VueApolloComposable.UseQueryOptions<MeQuery, MeQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MeQuery, MeQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MeQuery, MeQueryVariables>> = {}) {
return VueApolloComposable.useQuery<MeQuery, MeQueryVariables>(MeDocument, {}, options);
}
export function useMeLazyQuery(options: VueApolloComposable.UseQueryOptions<MeQuery, MeQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MeQuery, MeQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MeQuery, MeQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, {}, options);
}
export type MeQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<MeQuery, MeQueryVariables>;
export const RegisterSelfDocument = gql` export const RegisterSelfDocument = gql`
mutation RegisterSelf($input: RegisterSelfInput!) { mutation RegisterSelf($input: RegisterSelfInput!) {
registerSelf(input: $input) { registerSelf(input: $input) {

View File

@@ -1,16 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable'; import { useMutation, useQuery } from '@vue/apollo-composable';
import { import {
MeDocument,
MyMessengerConnectionsDocument, MyMessengerConnectionsDocument,
MyNotificationHistoryDocument, MyNotificationHistoryDocument,
SendTestMessengerMessageDocument, SendTestMessengerMessageDocument,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
const selectedChannel = ref<'TELEGRAM' | 'MAX'>('TELEGRAM'); const selectedChannel = ref<'TELEGRAM' | 'MAX'>('TELEGRAM');
const channelId = ref('');
const customMessage = ref('Тест канала уведомлений Fregat'); const customMessage = ref('Тест канала уведомлений Fregat');
const feedback = ref(''); const feedback = ref('');
const config = useRuntimeConfig();
const meQuery = useQuery(MeDocument);
const connectionsQuery = useQuery(MyMessengerConnectionsDocument); const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
const historyQuery = useQuery(MyNotificationHistoryDocument, () => ({ const historyQuery = useQuery(MyNotificationHistoryDocument, () => ({
channel: selectedChannel.value, channel: selectedChannel.value,
@@ -33,11 +35,43 @@ const activeConnection = computed(() =>
), ),
); );
const telegramConnection = computed(() =>
connectionsQuery.result.value?.myMessengerConnections?.find(
(item: { type: 'TELEGRAM' | 'MAX'; isActive: boolean; channelId: string }) =>
item.type === 'TELEGRAM' && item.isActive,
),
);
const maxConnection = computed(() =>
connectionsQuery.result.value?.myMessengerConnections?.find(
(item: { type: 'TELEGRAM' | 'MAX'; isActive: boolean; channelId: string }) =>
item.type === 'MAX' && item.isActive,
),
);
function buildBotConnectUrl(baseUrl: string) {
const email = meQuery.result.value?.me?.email?.trim().toLowerCase();
if (!email || !baseUrl) {
return '';
}
const payload = encodeURIComponent(`login:${email}`);
const separator = baseUrl.includes('?') ? '&' : '?';
return `${baseUrl}${separator}start=${payload}`;
}
const telegramConnectUrl = computed(() => buildBotConnectUrl(config.public.telegramBotUrl || ''));
const maxConnectUrl = computed(() => buildBotConnectUrl(config.public.maxBotUrl || ''));
async function sendTest() { async function sendTest() {
feedback.value = ''; feedback.value = '';
if (!activeConnection.value) {
feedback.value = `Канал ${selectedChannel.value} ещё не подключен. Сначала подключите его через бота.`;
return;
}
const result = await sendTestMutation.mutate({ const result = await sendTestMutation.mutate({
type: selectedChannel.value, type: selectedChannel.value,
channelId: channelId.value || undefined,
message: customMessage.value || undefined, message: customMessage.value || undefined,
}); });
@@ -67,6 +101,62 @@ async function sendTest() {
<div class="surface-card rounded-3xl p-5"> <div class="surface-card rounded-3xl p-5">
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-xl font-bold text-[#123824]">Подключение каналов</h2>
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p class="font-semibold">Telegram</p>
<p class="text-sm opacity-80">
{{
telegramConnection
? `Подключен: ${telegramConnection.channelId}`
: 'Не подключен'
}}
</p>
</div>
<a
:href="telegramConnectUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn btn-secondary"
:class="{ 'btn-disabled pointer-events-none': !telegramConnectUrl }"
>
{{ telegramConnection ? 'Переподключить Telegram' : 'Подключить Telegram' }}
</a>
</div>
</div>
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p class="font-semibold">Max</p>
<p class="text-sm opacity-80">
{{
maxConnection
? `Подключен: ${maxConnection.channelId}`
: 'Не подключен'
}}
</p>
</div>
<a
:href="maxConnectUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn btn-accent"
:class="{ 'btn-disabled pointer-events-none': !maxConnectUrl }"
>
{{ maxConnection ? 'Переподключить Max' : 'Подключить Max' }}
</a>
</div>
</div>
</div>
</div>
<div class="surface-card rounded-3xl p-5">
<div class="space-y-3">
<div class="tabs tabs-boxed w-fit"> <div class="tabs tabs-boxed w-fit">
<button <button
class="tab" class="tab"
@@ -91,15 +181,6 @@ async function sendTest() {
</span> </span>
</p> </p>
<label class="form-control">
<span class="label-text">Канал (опционально)</span>
<input
v-model="channelId"
class="input input-bordered border-[#d0e8d8] bg-white/80"
placeholder="если пусто, берется активный подключенный канал"
>
</label>
<label class="form-control"> <label class="form-control">
<span class="label-text">Тестовое сообщение</span> <span class="label-text">Тестовое сообщение</span>
<textarea v-model="customMessage" class="textarea textarea-bordered border-[#d0e8d8] bg-white/80" rows="3" /> <textarea v-model="customMessage" class="textarea textarea-bordered border-[#d0e8d8] bg-white/80" rows="3" />
@@ -107,7 +188,7 @@ async function sendTest() {
<button <button
class="btn w-fit border-0 bg-[#139957] text-white hover:bg-[#0d854a]" class="btn w-fit border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sendTestMutation.loading.value" :disabled="sendTestMutation.loading.value || !activeConnection"
@click="sendTest" @click="sendTest"
> >
Отправить тест Отправить тест
@@ -116,11 +197,7 @@ async function sendTest() {
<div v-if="feedback" class="alert" :class="feedback.startsWith('Ошибка') ? 'alert-error' : 'alert-success'"> <div v-if="feedback" class="alert" :class="feedback.startsWith('Ошибка') ? 'alert-error' : 'alert-success'">
{{ feedback }} {{ feedback }}
</div> </div>
</div>
</div>
<div class="surface-card rounded-3xl p-5">
<div class="space-y-3">
<h2 class="text-xl font-bold text-[#123824]">История по каналу {{ selectedChannel }}</h2> <h2 class="text-xl font-bold text-[#123824]">История по каналу {{ selectedChannel }}</h2>
<div v-if="historyQuery.loading.value" class="alert border-0 bg-white/75">Загрузка истории...</div> <div v-if="historyQuery.loading.value" class="alert border-0 bg-white/75">Загрузка истории...</div>
<div v-else-if="(historyQuery.result.value?.myNotificationHistory?.length ?? 0) === 0" class="alert"> <div v-else-if="(historyQuery.result.value?.myNotificationHistory?.length ?? 0) === 0" class="alert">

View File

@@ -0,0 +1,6 @@
query Me {
me {
id
email
}
}

View File

@@ -0,0 +1,8 @@
query MyMessengerConnections {
myMessengerConnections {
id
type
channelId
isActive
}
}

View File

@@ -0,0 +1,10 @@
query MyNotificationHistory($channel: MessengerType!, $limit: Int) {
myNotificationHistory(channel: $channel, limit: $limit) {
id
channel
title
message
createdAt
orderId
}
}

View File

@@ -0,0 +1,9 @@
mutation SendTestMessengerMessage($type: MessengerType!, $channelId: String, $message: String) {
sendTestMessengerMessage(type: $type, channelId: $channelId, message: $message) {
type
channelId
success
detail
sentAt
}
}