Files
web-frontend/app/pages/profile/notifications.vue
2026-04-06 15:51:10 +07:00

311 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>