311 lines
12 KiB
Vue
311 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||
import {
|
||
DeleteMyMessengerConnectionDocument,
|
||
MeDocument,
|
||
MyMessengerConnectionsDocument,
|
||
} from '~/composables/graphql/generated';
|
||
import {
|
||
messengerConnectionAvatarSrc,
|
||
messengerConnectionHandle,
|
||
messengerConnectionInitials,
|
||
messengerConnectionName,
|
||
} from '~/composables/useMessengerConnectionPresentation';
|
||
import { useMessengerStart } from '~/composables/useMessengerStart';
|
||
|
||
type MessengerChannel = 'TELEGRAM' | 'MAX';
|
||
|
||
type MessengerItem = {
|
||
id: string;
|
||
type: MessengerChannel;
|
||
isActive: boolean;
|
||
channelId: string;
|
||
displayName?: string | null;
|
||
username?: string | null;
|
||
avatarAvailable?: boolean | null;
|
||
};
|
||
|
||
type MessengerOption = {
|
||
channel: MessengerChannel;
|
||
label: string;
|
||
title: string;
|
||
description: string;
|
||
buttonClass: string;
|
||
iconClass: string;
|
||
unavailableText: string;
|
||
};
|
||
|
||
const config = useRuntimeConfig();
|
||
const feedback = ref('');
|
||
const meQuery = useQuery(MeDocument);
|
||
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
|
||
const deleteConnectionMutation = useMutation(DeleteMyMessengerConnectionDocument);
|
||
const { openMessengerBot, pendingChannel } = useMessengerStart();
|
||
|
||
const telegramConnection = computed(() =>
|
||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||
(item: MessengerItem) => item.type === 'TELEGRAM' && item.isActive,
|
||
) ?? null,
|
||
);
|
||
|
||
const maxConnection = computed(() =>
|
||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||
(item: MessengerItem) => item.type === 'MAX' && item.isActive,
|
||
) ?? null,
|
||
);
|
||
|
||
const messengerOptions: MessengerOption[] = [
|
||
{
|
||
channel: 'TELEGRAM',
|
||
label: 'Telegram',
|
||
title: 'Подключить Telegram',
|
||
description: 'Получайте статусы заказов и сервисные уведомления в Telegram.',
|
||
buttonClass: 'bg-[#1a9c63] text-white hover:bg-[#148553]',
|
||
iconClass: 'bg-[#123824] text-white',
|
||
unavailableText: 'Telegram пока не настроен в окружении фронта.',
|
||
},
|
||
{
|
||
channel: 'MAX',
|
||
label: 'MAX',
|
||
title: 'Подключить MAX',
|
||
description: 'Открывает MAX-бота и привязывает аккаунт к личному кабинету.',
|
||
buttonClass: 'bg-[#2b7fff] text-white hover:bg-[#1d6df1]',
|
||
iconClass: 'bg-[#2b7fff] text-white',
|
||
unavailableText: 'MAX пока не настроен в окружении фронта.',
|
||
},
|
||
];
|
||
|
||
function buildBotConnectUrl(baseUrl: string) {
|
||
const accountEmail = meQuery.result.value?.me?.email?.trim().toLowerCase();
|
||
if (!accountEmail || !baseUrl) {
|
||
return '';
|
||
}
|
||
|
||
return baseUrl;
|
||
}
|
||
|
||
const telegramConnectUrl = computed(() => buildBotConnectUrl(config.public.telegramBotUrl || ''));
|
||
const maxConnectUrl = computed(() => buildBotConnectUrl(config.public.maxBotUrl || ''));
|
||
|
||
function connectUrl(channel: MessengerChannel) {
|
||
return channel === 'TELEGRAM' ? telegramConnectUrl.value : maxConnectUrl.value;
|
||
}
|
||
|
||
function connectionFor(channel: MessengerChannel) {
|
||
return channel === 'TELEGRAM' ? telegramConnection.value : maxConnection.value;
|
||
}
|
||
|
||
const activeConnections = computed(() => messengerOptions
|
||
.map((option) => ({
|
||
option,
|
||
connection: connectionFor(option.channel),
|
||
}))
|
||
.filter((item) => Boolean(item.connection)));
|
||
|
||
const availableOptions = computed(() => messengerOptions
|
||
.filter((option) => !connectionFor(option.channel)));
|
||
|
||
async function connectMessenger(channel: MessengerChannel) {
|
||
feedback.value = '';
|
||
const baseUrl = connectUrl(channel);
|
||
|
||
if (!baseUrl) {
|
||
feedback.value = channel === 'MAX'
|
||
? 'MAX не откроется, пока не задан NUXT_PUBLIC_MAX_BOT_URL.'
|
||
: 'Telegram не откроется, пока не задан NUXT_PUBLIC_TELEGRAM_BOT_URL.';
|
||
return;
|
||
}
|
||
|
||
await openMessengerBot({
|
||
channel,
|
||
baseUrl,
|
||
redirectPath: `/profile/notifications/success?connected=${channel.toLowerCase()}`,
|
||
});
|
||
}
|
||
|
||
async function removeConnection(connectionId: string) {
|
||
feedback.value = '';
|
||
const result = await deleteConnectionMutation.mutate({
|
||
connectionId,
|
||
});
|
||
|
||
if (!result?.data?.deleteMyMessengerConnection) {
|
||
feedback.value = 'Не удалось отключить аккаунт. Попробуйте еще раз.';
|
||
return;
|
||
}
|
||
|
||
await connectionsQuery.refetch();
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="space-y-6">
|
||
<NuxtLink to="/profile" class="link link-hover text-sm">← Назад в профиль</NuxtLink>
|
||
|
||
<div class="space-y-2">
|
||
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Уведомления</h1>
|
||
<p class="max-w-3xl text-sm leading-6 text-[#466653]">
|
||
Подключите удобные мессенджеры, чтобы получать статусы заказов и важные уведомления без лишних переходов в кабинет.
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
v-if="feedback"
|
||
class="rounded-[24px] border px-4 py-3 text-sm font-medium"
|
||
:class="feedback.includes('Не удалось') || feedback.includes('не откроется')
|
||
? 'border-[#f1d1c7] bg-[#fff3ef] text-[#9d4426]'
|
||
: 'border-[#cbe9d6] bg-[#f1fbf5] text-[#1c6b45]'"
|
||
>
|
||
{{ feedback }}
|
||
</div>
|
||
|
||
<div
|
||
v-if="activeConnections.length === 0"
|
||
class="rounded-[32px] bg-[#edf3ee] p-6 md:p-8"
|
||
>
|
||
<div class="space-y-3">
|
||
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Подключите мессенджеры</h2>
|
||
<p class="max-w-3xl text-sm leading-6 text-[#557562]">
|
||
Вы можете подключить любой из мессенджеров ниже. После подключения уведомления о заказах и важных действиях будут приходить прямо туда.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||
<button
|
||
v-for="option in messengerOptions"
|
||
:key="option.channel"
|
||
class="flex min-h-[120px] flex-col items-start justify-between rounded-[28px] border-0 px-5 py-5 text-left shadow-[0_18px_38px_rgba(18,56,36,0.08)] transition"
|
||
:class="[option.buttonClass, { 'opacity-60': !connectUrl(option.channel) }]"
|
||
:disabled="pendingChannel === option.channel || !connectUrl(option.channel)"
|
||
@click="connectMessenger(option.channel)"
|
||
>
|
||
<div class="space-y-2">
|
||
<div class="inline-flex h-11 w-11 items-center justify-center rounded-2xl bg-white/18 text-sm font-black text-white">
|
||
{{ option.channel === 'TELEGRAM' ? 'TG' : 'MX' }}
|
||
</div>
|
||
<div>
|
||
<p class="text-lg font-black tracking-[-0.03em]">{{ option.title }}</p>
|
||
<p class="mt-1 text-sm text-white/82">{{ option.description }}</p>
|
||
</div>
|
||
</div>
|
||
<p class="text-sm font-semibold text-white/90">
|
||
{{ pendingChannel === option.channel ? `Открываем ${option.label}...` : `Перейти в ${option.label}` }}
|
||
</p>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mt-4 space-y-2">
|
||
<p
|
||
v-for="option in messengerOptions.filter((item) => !connectUrl(item.channel))"
|
||
:key="`${option.channel}-hint`"
|
||
class="text-sm text-[#8b5a49]"
|
||
>
|
||
{{ option.unavailableText }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<div class="rounded-[32px] bg-[#edf3ee] p-6 md:p-8">
|
||
<div class="space-y-3">
|
||
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Подключенные аккаунты</h2>
|
||
<p class="text-sm leading-6 text-[#557562]">
|
||
Здесь показаны активные мессенджеры, привязанные к вашему кабинету.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="mt-6 space-y-4">
|
||
<article
|
||
v-for="{ option, connection } in activeConnections"
|
||
:key="connection!.id"
|
||
class="rounded-[28px] bg-white px-5 py-4 shadow-[0_18px_38px_rgba(18,56,36,0.08)]"
|
||
>
|
||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||
<div class="flex min-w-0 items-center gap-4">
|
||
<div v-if="messengerConnectionAvatarSrc(connection)" class="avatar">
|
||
<div class="h-14 w-14 rounded-[20px]">
|
||
<img :src="messengerConnectionAvatarSrc(connection)" :alt="messengerConnectionName(connection)">
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else
|
||
class="flex h-14 w-14 items-center justify-center rounded-[20px] text-sm font-black"
|
||
:class="option.iconClass"
|
||
>
|
||
{{ messengerConnectionInitials(connection, option.channel === 'TELEGRAM' ? 'TG' : 'MX') }}
|
||
</div>
|
||
|
||
<div class="min-w-0">
|
||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">{{ option.label }}</p>
|
||
<p class="truncate text-lg font-bold text-[#123824]">{{ messengerConnectionName(connection) }}</p>
|
||
<p class="truncate text-sm text-[#557562]">{{ messengerConnectionHandle(connection) || connection.channelId }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
class="btn rounded-full border border-[#e5cfc7] bg-[#fff5f1] px-5 text-[#a64d2d] hover:border-[#deb5a8] hover:bg-[#ffe8e0]"
|
||
:disabled="deleteConnectionMutation.loading.value"
|
||
@click="removeConnection(connection!.id)"
|
||
>
|
||
{{ deleteConnectionMutation.loading.value ? 'Удаляем...' : 'Удалить' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="availableOptions.length > 0"
|
||
class="rounded-[32px] bg-[#edf3ee] p-6 md:p-8"
|
||
>
|
||
<div class="space-y-3">
|
||
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Можно подключить еще</h2>
|
||
<p class="text-sm leading-6 text-[#557562]">
|
||
Добавьте второй канал, чтобы не потерять уведомления, если один мессенджер недоступен.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||
<button
|
||
v-for="option in availableOptions"
|
||
:key="option.channel"
|
||
class="flex min-h-[112px] flex-col items-start justify-between rounded-[28px] border-0 px-5 py-5 text-left shadow-[0_18px_38px_rgba(18,56,36,0.08)] transition"
|
||
:class="[option.buttonClass, { 'opacity-60': !connectUrl(option.channel) }]"
|
||
:disabled="pendingChannel === option.channel || !connectUrl(option.channel)"
|
||
@click="connectMessenger(option.channel)"
|
||
>
|
||
<div>
|
||
<p class="text-lg font-black tracking-[-0.03em]">{{ option.title }}</p>
|
||
<p class="mt-2 text-sm text-white/82">{{ option.description }}</p>
|
||
</div>
|
||
<p class="text-sm font-semibold text-white/90">
|
||
{{ pendingChannel === option.channel ? `Открываем ${option.label}...` : `Подключить ${option.label}` }}
|
||
</p>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mt-4 space-y-2">
|
||
<p
|
||
v-for="option in availableOptions.filter((item) => !connectUrl(item.channel))"
|
||
:key="`${option.channel}-connected-hint`"
|
||
class="text-sm text-[#8b5a49]"
|
||
>
|
||
{{ option.unavailableText }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-else
|
||
class="rounded-[32px] bg-[linear-gradient(135deg,#123824_0%,#1a5635_100%)] p-6 text-white md:p-8"
|
||
>
|
||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/65">Готово</p>
|
||
<h2 class="mt-3 text-2xl font-black tracking-[-0.03em]">Оба канала подключены</h2>
|
||
<p class="mt-2 max-w-3xl text-sm leading-6 text-white/78">
|
||
Теперь важные уведомления будут доступны и в Telegram, и в MAX. Если захотите сменить аккаунт, удалите текущий и подключите новый.
|
||
</p>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</template>
|