Files
portal/app/resources/js/views/SettingsView.vue
T
Дмитрий 9ff19f3cc3 @
feat(bot): всеобъемлющая инструкция (36 статей) + 31 узкая экскурсия, которая открывает окна

Живой урок владельца 12.07.2026: «дубли чистятся в 2-х местах, а бот выдумал третье»,
«стыдно такого бота показывать», «видео показываем полное, а я спрашиваю про одно —
надо резать», «надо не только кнопку показывать, а и что за ней».

База знаний: 17 обзорных статей → 36 узких по темам. Портал обойдён целиком
(5 агентов): все экраны, окна, подсказки «(?)», тексты ошибок, внутренние правила.
Новое, чего бот не знал: дубли в ДВУХ местах (Поле и Предложения) + окно «источник
уже есть»; правило 18:00; почему приходит меньше лимита; оплата по счёту и акты;
списания; безопасность и 2FA; отчёты; массовые действия; регистрация.

Экскурсии: 15 → 31, каждая на свою тему (3–7 шагов). Раннер GuidedTour научен
шагу `open` — сам открывает окно/вкладку и подсвечивает то, что ВНУТРИ
(окно дублей, форму подбора, диалог пополнения, панель тарифов, карточку сделки).
99 новых якорей data-tour расставлено по всему порталу (4 агента).

Поиск: веса в tsvector (заголовок A / синонимы B / текст C) — иначе на «чистка
дублей» первой всплывала статья «Списания» из-за мимоходного упоминания «поля».

Тесты: Pest бот 39/39, Vitest 1222/1222. Новые сторожа: у каждой статьи есть
экскурсия; экскурсия не короче 3 и не длиннее 7 шагов; каждая цель экскурсии
реально существует в разметке; экскурсии про действия обязаны открывать окно.
Живая проверка в браузере: экскурсия про дубли сама открыла окно объединения
и перешла на второе место чистки.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@
2026-07-12 12:56:22 +03:00

115 lines
4.4 KiB
Vue

<script setup lang="ts">
/**
* Settings — настройки тенанта/пользователя. 4 рабочие вкладки.
*
* Источник дизайна: liderra_v8_handoff/concepts/v8_settings.html.
* Полностью реализованы (с UI-разводкой): Профиль, Безопасность, API и Webhook,
* Уведомления (матрица 8×3 по schema v8.7 §4 users.notification_preferences).
*
* Аудит D6/D7 (Sprint 3E, 2026-05-16): placeholder-вкладки Проекты/Команда/
* Интеграции/Тихие часы убраны — UI не должен обещать «в разработке».
* «Проекты» дублировали /projects; «Команда» и «Тихие часы» (ТЗ §17.8)
* требуют schema+backend (отдельные эпики); «Интеграции» внешне-блокированы (Б-1).
* Вкладки вернутся при реальной реализации соответствующих модулей.
*/
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import ApiTab from './settings/ApiTab.vue';
import NotificationsTab from './settings/NotificationsTab.vue';
import ProfileTab from './settings/ProfileTab.vue';
import RequisitesTab from './settings/RequisitesTab.vue';
import SecurityTab from './settings/SecurityTab.vue';
interface Tab {
id: string;
label: string;
icon: string;
}
const tabs: Tab[] = [
{ id: 'profile', label: 'Профиль', icon: 'mdi-account-outline' },
{ id: 'requisites', label: 'Реквизиты', icon: 'mdi-file-document-outline' },
{ id: 'security', label: 'Безопасность', icon: 'mdi-shield-lock-outline' },
{ id: 'api', label: 'API и Webhook', icon: 'mdi-api' },
{ id: 'notifications', label: 'Уведомления', icon: 'mdi-bell-outline' },
];
const activeTab = ref('profile');
// Якоря для экскурсий портала (data-tour) — по одному на каждую вкладку рейла.
const tabTourAnchors: Record<string, string> = {
profile: 'settings-tab-profile',
security: 'settings-tab-security',
notifications: 'settings-tab-notifications',
requisites: 'settings-tab-requisites',
api: 'settings-tab-api',
};
const route = useRoute();
onMounted(() => {
const t = route.query.tab;
if (typeof t === 'string' && tabs.some((tab) => tab.id === t)) {
activeTab.value = t;
}
});
</script>
<template>
<v-container fluid class="settings pa-6">
<header class="page-head">
<h1 class="text-h4 mb-2 page-title">Настройки</h1>
<p class="text-body-2 text-medium-emphasis ma-0">Профиль, безопасность, API и интеграции</p>
</header>
<v-row class="settings-row mt-4">
<v-col cols="12" md="3">
<v-card variant="outlined" class="tabs-rail pa-2">
<v-list density="compact" nav>
<v-list-item
v-for="tab in tabs"
:key="tab.id"
:prepend-icon="tab.icon"
:active="activeTab === tab.id"
:data-tour="tabTourAnchors[tab.id]"
rounded="lg"
@click="activeTab = tab.id"
>
<v-list-item-title>{{ tab.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-col>
<v-col cols="12" md="9">
<v-card variant="outlined" class="tab-pane pa-6">
<ProfileTab v-if="activeTab === 'profile'" />
<RequisitesTab v-else-if="activeTab === 'requisites'" />
<SecurityTab v-else-if="activeTab === 'security'" />
<ApiTab v-else-if="activeTab === 'api'" />
<NotificationsTab v-else-if="activeTab === 'notifications'" />
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.settings {
max-width: 1440px;
}
.page-title {
font-variation-settings: 'opsz' 28;
letter-spacing: -0.018em;
}
.tabs-rail {
background: #fff;
}
.tab-pane {
background: #fff;
min-height: 480px;
}
</style>