Files
web-frontend/app/pages/notifications.vue
2026-04-06 14:41:32 +07:00

295 lines
11 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 {
MeDocument,
MyMessengerConnectionsDocument,
MyNotificationHistoryDocument,
SendTestMessengerMessageDocument,
} from '~/composables/graphql/generated';
import {
messengerConnectionAvatarSrc,
messengerConnectionHandle,
messengerConnectionInitials,
messengerConnectionName,
} from '~/composables/useMessengerConnectionPresentation';
import { useMessengerStart } from '~/composables/useMessengerStart';
const selectedChannel = ref<'TELEGRAM' | 'MAX'>('TELEGRAM');
const customMessage = ref('Тест канала уведомлений Fregat');
const feedback = ref('');
const config = useRuntimeConfig();
const { openMessengerBot, pendingChannel } = useMessengerStart();
const meQuery = useQuery(MeDocument);
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
const historyQuery = useQuery(MyNotificationHistoryDocument, () => ({
channel: selectedChannel.value,
limit: 50,
}));
const sendTestMutation = useMutation(SendTestMessengerMessageDocument);
watch(selectedChannel, () => {
feedback.value = '';
historyQuery.refetch({
channel: selectedChannel.value,
limit: 50,
});
});
const activeConnection = computed(() =>
connectionsQuery.result.value?.myMessengerConnections?.find(
(item: { type: 'TELEGRAM' | 'MAX'; isActive: boolean; channelId: string }) =>
item.type === selectedChannel.value && item.isActive,
),
);
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 '';
}
return baseUrl;
}
const telegramConnectUrl = computed(() => buildBotConnectUrl(config.public.telegramBotUrl || ''));
const maxConnectUrl = computed(() => buildBotConnectUrl(config.public.maxBotUrl || ''));
async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
const baseUrl = channel === 'TELEGRAM' ? telegramConnectUrl.value : maxConnectUrl.value;
if (!baseUrl) {
return;
}
await openMessengerBot({
channel,
baseUrl,
redirectPath: `/profile/notifications/success?connected=${channel.toLowerCase()}`,
});
}
async function sendTest() {
feedback.value = '';
if (!activeConnection.value) {
feedback.value = `Канал ${selectedChannel.value} ещё не подключен. Сначала подключите его через бота.`;
return;
}
const result = await sendTestMutation.mutate({
type: selectedChannel.value,
message: customMessage.value || undefined,
});
const payload = result?.data?.sendTestMessengerMessage;
if (!payload) {
feedback.value = 'Не удалось отправить тестовое сообщение.';
return;
}
feedback.value = payload.success
? `Отправлено в ${payload.type}: ${payload.detail}`
: `Ошибка отправки: ${payload.detail}`;
await historyQuery.refetch({
channel: selectedChannel.value,
limit: 50,
});
}
</script>
<template>
<section class="space-y-6">
<div>
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Уведомления</h1>
</div>
<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="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 class="min-w-0">
<p class="font-semibold">Telegram</p>
<div v-if="telegramConnection" class="mt-3 flex items-center gap-3">
<div v-if="messengerConnectionAvatarSrc(telegramConnection)" class="avatar">
<div class="h-11 w-11 rounded-full">
<img :src="messengerConnectionAvatarSrc(telegramConnection)" :alt="messengerConnectionName(telegramConnection)">
</div>
</div>
<div v-else class="avatar placeholder">
<div class="h-11 w-11 rounded-full bg-[#123824] text-sm font-bold text-white">
<span>{{ messengerConnectionInitials(telegramConnection, 'TG') }}</span>
</div>
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-[#123824]">
{{ messengerConnectionName(telegramConnection) }}
</p>
<p class="truncate text-xs text-[#5c7b69]">
{{ messengerConnectionHandle(telegramConnection) || 'Подключен' }}
</p>
</div>
</div>
<p v-else class="text-sm opacity-80">Не подключен</p>
</div>
<button
class="btn btn-secondary"
:class="{ 'btn-disabled pointer-events-none': !telegramConnectUrl }"
:disabled="pendingChannel === 'TELEGRAM' || !telegramConnectUrl"
@click="connectMessenger('TELEGRAM')"
>
{{
pendingChannel === 'TELEGRAM'
? 'Открываем Telegram…'
: telegramConnection
? 'Переподключить Telegram'
: 'Подключить Telegram'
}}
</button>
</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 class="min-w-0">
<p class="font-semibold">Max</p>
<div v-if="maxConnection" class="mt-3 flex items-center gap-3">
<div class="avatar placeholder">
<div class="h-11 w-11 rounded-full bg-[#2b7fff] text-sm font-bold text-white">
<span>{{ messengerConnectionInitials(maxConnection, 'MX') }}</span>
</div>
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-[#123824]">
{{ messengerConnectionName(maxConnection) }}
</p>
<p class="truncate text-xs text-[#5c7b69]">
{{ messengerConnectionHandle(maxConnection) || 'Подключен' }}
</p>
</div>
</div>
<p v-else class="text-sm opacity-80">Не подключен</p>
</div>
<button
class="btn btn-accent"
:class="{ 'btn-disabled pointer-events-none': !maxConnectUrl }"
:disabled="pendingChannel === 'MAX' || !maxConnectUrl"
@click="connectMessenger('MAX')"
>
{{
pendingChannel === 'MAX'
? 'Открываем Max…'
: maxConnection
? 'Переподключить Max'
: 'Подключить Max'
}}
</button>
</div>
</div>
</div>
</div>
<div class="surface-card rounded-3xl p-5">
<div class="space-y-3">
<div class="tabs tabs-boxed w-fit">
<button
class="tab"
:class="{ 'tab-active': selectedChannel === 'TELEGRAM' }"
@click="selectedChannel = 'TELEGRAM'"
>
Telegram
</button>
<button
class="tab"
:class="{ 'tab-active': selectedChannel === 'MAX' }"
@click="selectedChannel = 'MAX'"
>
Max
</button>
</div>
<p class="text-sm opacity-80">
Активный канал:
<span class="font-semibold">
{{ activeConnection ? activeConnection.channelId : 'не подключен' }}
</span>
</p>
<label class="form-control">
<span class="label-text">Тестовое сообщение</span>
<textarea v-model="customMessage" class="textarea textarea-bordered" rows="3" />
</label>
<button
class="btn w-fit border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sendTestMutation.loading.value || !activeConnection"
@click="sendTest"
>
Отправить тест
</button>
<div v-if="feedback" class="alert" :class="feedback.startsWith('Ошибка') ? 'alert-error' : 'alert-success'">
{{ feedback }}
</div>
<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-else-if="(historyQuery.result.value?.myNotificationHistory?.length ?? 0) === 0" class="alert">
История пока пустая.
</div>
<ul v-else class="space-y-3">
<li
v-for="item in historyQuery.result.value?.myNotificationHistory ?? []"
:key="item.id"
class="rounded-xl border border-[#d6ebde] bg-white/75 p-3"
>
<p class="font-semibold">{{ item.title }}</p>
<p class="text-sm opacity-80">{{ item.message }}</p>
<p class="text-xs opacity-60">{{ new Date(item.createdAt).toLocaleString() }}</p>
</li>
</ul>
</div>
</div>
</div>
</section>
</template>