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

362 lines
16 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 { useQuery } from '@vue/apollo-composable';
import {
MeDocument,
MyMessengerConnectionsDocument,
} from '~/composables/graphql/generated';
type DeliveryChannel = 'EMAIL' | 'TELEGRAM' | 'MAX';
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[]>(() => {
const companyGreeting = me.value?.fullName || 'Клиент Фрегат';
return [
{
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) {
if (channel === 'EMAIL') {
return 'Email';
}
if (channel === 'TELEGRAM') {
return 'Telegram';
}
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>
<template>
<section class="space-y-6">
<div class="manager-hero">
<p class="manager-eyebrow">Message Board</p>
<h1 class="manager-title">Локальная витрина уведомлений</h1>
<p class="manager-copy">
Здесь собраны шаблоны сообщений для email, Telegram и Max: сами тексты, кнопки и точки входа.
Это удобная доска, чтобы быстро согласовать контент до полной привязки к продовым событиям.
</p>
</div>
<div class="surface-card rounded-3xl p-5">
<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 class="space-y-5">
<article
v-for="scenario in messageScenarios"
:key="scenario.id"
class="surface-card rounded-3xl p-5"
>
<div class="space-y-2">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<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">
<section
v-for="preview in scenario.previews"
:key="`${scenario.id}-${preview.channel}`"
class="rounded-[28px] border border-[#deebe4] bg-[#fbfdfb] p-4"
>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em]" :class="channelBadgeClass(preview.channel)">
{{ channelLabel(preview.channel) }}
</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)]">
<p v-if="preview.subject" class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">
{{ preview.subject }}
</p>
<div class="mt-3 space-y-3 text-sm leading-6 text-[#123824]">
<p
v-for="line in preview.body"
:key="line"
>
{{ line }}
</p>
</div>
<div v-if="preview.buttonLabel" class="mt-4">
<NuxtLink
v-if="preview.buttonTo"
:to="preview.buttonTo"
class="btn h-11 rounded-full border-0 bg-[#139957] px-5 text-white hover:bg-[#0d854a]"
>
{{ preview.buttonLabel }}
</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>
<p class="mt-4 text-sm leading-6 text-[#557562]">{{ preview.note }}</p>
</section>
</div>
</article>
</div>
</section>
</template>