Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b04e7e752 | |||
| 822e5346d8 |
@@ -1,16 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Settings — настройки тенанта/пользователя. 8 вкладок (по v8.5 §13 + ТЗ §14).
|
||||
* Settings — настройки тенанта/пользователя. 4 рабочие вкладки.
|
||||
*
|
||||
* Источник дизайна: liderra_v8_handoff/concepts/v8_settings.html.
|
||||
* Полностью реализованы (с UI-разводкой): Профиль, Безопасность, API и Webhook,
|
||||
* Уведомления (матрица 8×3 по schema v8.7 §4 users.notification_preferences).
|
||||
* Placeholder-заглушки: Проекты, Команда, Интеграции, Тихие часы.
|
||||
*
|
||||
* Аудит D6/D7 (Sprint 3E, 2026-05-16): placeholder-вкладки Проекты/Команда/
|
||||
* Интеграции/Тихие часы убраны — UI не должен обещать «в разработке».
|
||||
* «Проекты» дублировали /projects; «Команда» и «Тихие часы» (ТЗ §17.8)
|
||||
* требуют schema+backend (отдельные эпики); «Интеграции» внешне-блокированы (Б-1).
|
||||
* Вкладки вернутся при реальной реализации соответствующих модулей.
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import ApiTab from './settings/ApiTab.vue';
|
||||
import NotificationsTab from './settings/NotificationsTab.vue';
|
||||
import PlaceholderTab from './settings/PlaceholderTab.vue';
|
||||
import ProfileTab from './settings/ProfileTab.vue';
|
||||
import SecurityTab from './settings/SecurityTab.vue';
|
||||
|
||||
@@ -23,41 +27,11 @@ interface Tab {
|
||||
const tabs: Tab[] = [
|
||||
{ id: 'profile', label: 'Профиль', icon: 'mdi-account-outline' },
|
||||
{ id: 'security', label: 'Безопасность', icon: 'mdi-shield-lock-outline' },
|
||||
{ id: 'projects', label: 'Проекты', icon: 'mdi-folder-outline' },
|
||||
{ id: 'team', label: 'Команда', icon: 'mdi-account-group-outline' },
|
||||
{ id: 'api', label: 'API и Webhook', icon: 'mdi-api' },
|
||||
{ id: 'integrations', label: 'Интеграции', icon: 'mdi-puzzle-outline' },
|
||||
{ id: 'hours', label: 'Тихие часы', icon: 'mdi-clock-outline' },
|
||||
{ id: 'notifications', label: 'Уведомления', icon: 'mdi-bell-outline' },
|
||||
];
|
||||
|
||||
const activeTab = ref('profile');
|
||||
|
||||
const placeholderProps = computed(() => {
|
||||
const map: Record<string, { title: string; description: string }> = {
|
||||
projects: {
|
||||
title: 'Проекты',
|
||||
description:
|
||||
'Управление проектами тенанта (макс. 10 на тарифе «Команда»). Для каждого проекта — поставщик ГЦК, цена за лид, активные UTM-кампании.',
|
||||
},
|
||||
team: {
|
||||
title: 'Команда',
|
||||
description:
|
||||
'Менеджеры тенанта (макс. 4 + расширение). Назначение прав, автораспределение, ограничение доступа к проектам.',
|
||||
},
|
||||
integrations: {
|
||||
title: 'Интеграции',
|
||||
description:
|
||||
'Подключение Telegram-бота для нотификаций, экспорт в 1С 8.3, JivoSite helpdesk, Yandex 360 SSO.',
|
||||
},
|
||||
hours: {
|
||||
title: 'Тихие часы',
|
||||
description:
|
||||
'Расписание, в которое не приходят SMS/звонки автонапоминаний (например, 22:00-08:00 + выходные).',
|
||||
},
|
||||
};
|
||||
return map[activeTab.value];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -91,11 +65,6 @@ const placeholderProps = computed(() => {
|
||||
<SecurityTab v-else-if="activeTab === 'security'" />
|
||||
<ApiTab v-else-if="activeTab === 'api'" />
|
||||
<NotificationsTab v-else-if="activeTab === 'notifications'" />
|
||||
<PlaceholderTab
|
||||
v-else-if="placeholderProps"
|
||||
:title="placeholderProps.title"
|
||||
:description="placeholderProps.description"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Универсальный placeholder для ещё-не-реализованных вкладок Settings.
|
||||
* Используется для вкладок: Проекты, Команда, Интеграции, Тихие часы.
|
||||
*
|
||||
* При реализации каждой вкладки — заменяется на отдельный component.
|
||||
*/
|
||||
defineProps<{ title: string; description: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
<h2 class="tab-title text-h6 mb-3">{{ title }}</h2>
|
||||
<v-alert type="info" variant="tonal" density="compact" class="mb-4">
|
||||
<strong>В разработке.</strong> Этот раздел реализуется в следующих коммитах.
|
||||
</v-alert>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tab-title {
|
||||
font-variation-settings: 'opsz' 18;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
</style>
|
||||
@@ -15,28 +15,26 @@ describe('SettingsView.vue', () => {
|
||||
expect(wrapper.find('h1').text()).toBe('Настройки');
|
||||
});
|
||||
|
||||
it('содержит ровно 8 nav-tabs', () => {
|
||||
it('содержит ровно 4 nav-tabs (placeholder-вкладки убраны, audit D6/D7)', () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
expect(items.length).toBe(8);
|
||||
expect(items.length).toBe(4);
|
||||
});
|
||||
|
||||
it('содержит все 8 названий вкладок', () => {
|
||||
it('содержит все 4 названия рабочих вкладок', () => {
|
||||
const wrapper = factory();
|
||||
const text = wrapper.text();
|
||||
const labels = [
|
||||
'Профиль',
|
||||
'Безопасность',
|
||||
'Проекты',
|
||||
'Команда',
|
||||
'API и Webhook',
|
||||
'Интеграции',
|
||||
'Тихие часы',
|
||||
'Уведомления',
|
||||
];
|
||||
const labels = ['Профиль', 'Безопасность', 'API и Webhook', 'Уведомления'];
|
||||
labels.forEach((l) => expect(text).toContain(l));
|
||||
});
|
||||
|
||||
it('не содержит placeholder-вкладок и текста «В разработке»', () => {
|
||||
const wrapper = factory();
|
||||
const railText = wrapper.find('.tabs-rail').text();
|
||||
['Команда', 'Интеграции', 'Тихие часы'].forEach((l) => expect(railText).not.toContain(l));
|
||||
expect(wrapper.text()).not.toContain('В разработке');
|
||||
});
|
||||
|
||||
it('по умолчанию показывает вкладку «Профиль»', () => {
|
||||
const wrapper = factory();
|
||||
const text = wrapper.text();
|
||||
@@ -46,17 +44,6 @@ describe('SettingsView.vue', () => {
|
||||
expect(text).toContain('Тайм-зона');
|
||||
});
|
||||
|
||||
it('placeholder-вкладки показывают «В разработке»', async () => {
|
||||
const wrapper = factory();
|
||||
// Кликаем по «Проекты» — placeholder-вкладка.
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
const projectsItem = items.find((i) => i.text().includes('Проекты'));
|
||||
expect(projectsItem).toBeDefined();
|
||||
await projectsItem!.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.text()).toContain('В разработке');
|
||||
});
|
||||
|
||||
it('переключение на «Уведомления» показывает матрицу 8×3', async () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# Sprint 3E — Settings placeholder-tabs (D6/D7) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Убрать из `SettingsView` 4 placeholder-вкладки («Проекты», «Команда», «Интеграции», «Тихие часы»), которые показывают «В разработке» — UI не должен обещать нереализованный функционал.
|
||||
|
||||
**Architecture:** Чистое удаление. `SettingsView` оставляет 4 рабочие вкладки (Профиль, Безопасность, API и Webhook, Уведомления). Компонент `PlaceholderTab.vue` удаляется целиком. Spec-тест приводится к 4-вкладочному состоянию + добавляется регрессионная проверка, что placeholder'ы пропали.
|
||||
|
||||
**Tech Stack:** Vue 3 (`<script setup>` + TypeScript), Vuetify 3, Vitest 4 + @vue/test-utils.
|
||||
|
||||
---
|
||||
|
||||
## Контекст и per-tab решение (audit D6/D7)
|
||||
|
||||
Аудит портала ([docs/superpowers/specs/2026-05-15-portal-audit-design.md](../specs/2026-05-15-portal-audit-design.md)):
|
||||
|
||||
- **D6** — «PlaceholderTab × 4 — реализовать или скрыть (decide per-tab)».
|
||||
- **D7** — «SettingsView left-rail: 8 tab'ов, 4 заглушки — Hide-if-not-implemented».
|
||||
|
||||
**Per-tab решение — скрыть все 4** (реализация каждой = отдельный эпик, вне scope Sprint 3E):
|
||||
|
||||
| Вкладка | Решение | Обоснование |
|
||||
|---|---|---|
|
||||
| Проекты | скрыть | Полноценный `/projects` view уже есть — вкладка чистый дубль. |
|
||||
| Команда | скрыть | Нет ни `/team`-маршрута, ни backend; реализация = отдельный L-эпик со schema-работой, не в графике спринтов. |
|
||||
| Интеграции | скрыть | Telegram/1С/JivoSite/Yandex SSO — все внешне-блокированы (Б-1 и пр.). |
|
||||
| Тихие часы | скрыть | `quiet_hours` отсутствует в `db/schema.sql`; ТЗ §17.8 спецификацию даёт, но колонок/backend нет — отдельный эпик. |
|
||||
|
||||
«Импорт»-вкладка из предложения D7 — это H8 (Sprint 4, миграция §6), **вне scope Sprint 3E**.
|
||||
|
||||
Скрытие не отменяет ТЗ-требования (Команда / Тихие часы §17.8) — вкладки вернутся при реальной реализации соответствующих модулей.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `app/resources/js/views/SettingsView.vue` — убрать 4 placeholder-вкладки, `placeholderProps` computed, импорт и использование `PlaceholderTab`, неиспользуемый импорт `computed`; обновить docblock.
|
||||
- Delete: `app/resources/js/views/settings/PlaceholderTab.vue` — компонент больше не используется.
|
||||
- Test: `app/tests/Frontend/SettingsView.spec.ts` — 8 → 4 вкладки, убрать placeholder-тест, добавить регрессию.
|
||||
|
||||
**НЕ трогать:** `app/dev-indices.json` (авто-генерируемый временной DevIndex-фичей, уже `M` в git status — не стейджить, не коммитить); `SettingsView.story.vue` (ссылается только на `SettingsView`, не на `PlaceholderTab` — изменений не требует).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Скрыть 4 placeholder-вкладки в SettingsView
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `app/resources/js/views/SettingsView.vue`
|
||||
- Delete: `app/resources/js/views/settings/PlaceholderTab.vue`
|
||||
- Test: `app/tests/Frontend/SettingsView.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Привести spec-тест к 4-вкладочному состоянию (failing test first)**
|
||||
|
||||
Заменить весь файл `app/tests/Frontend/SettingsView.spec.ts` на:
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import SettingsView from '../../resources/js/views/SettingsView.vue';
|
||||
|
||||
describe('SettingsView.vue', () => {
|
||||
const factory = () =>
|
||||
mount(SettingsView, {
|
||||
global: { plugins: [createPinia(), createVuetify()] },
|
||||
});
|
||||
|
||||
it('монтируется и содержит заголовок «Настройки»', () => {
|
||||
const wrapper = factory();
|
||||
expect(wrapper.find('h1').text()).toBe('Настройки');
|
||||
});
|
||||
|
||||
it('содержит ровно 4 nav-tabs (placeholder-вкладки убраны, audit D6/D7)', () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
expect(items.length).toBe(4);
|
||||
});
|
||||
|
||||
it('содержит все 4 названия рабочих вкладок', () => {
|
||||
const wrapper = factory();
|
||||
const text = wrapper.text();
|
||||
const labels = ['Профиль', 'Безопасность', 'API и Webhook', 'Уведомления'];
|
||||
labels.forEach((l) => expect(text).toContain(l));
|
||||
});
|
||||
|
||||
it('не содержит placeholder-вкладок и текста «В разработке»', () => {
|
||||
const wrapper = factory();
|
||||
const railText = wrapper.find('.tabs-rail').text();
|
||||
['Команда', 'Интеграции', 'Тихие часы'].forEach((l) => expect(railText).not.toContain(l));
|
||||
expect(wrapper.text()).not.toContain('В разработке');
|
||||
});
|
||||
|
||||
it('по умолчанию показывает вкладку «Профиль»', () => {
|
||||
const wrapper = factory();
|
||||
const text = wrapper.text();
|
||||
// ProfileTab содержит поля Имя / Фамилия (split из «Полное имя» в audit D1) и Тайм-зона.
|
||||
expect(text).toContain('Имя');
|
||||
expect(text).toContain('Фамилия');
|
||||
expect(text).toContain('Тайм-зона');
|
||||
});
|
||||
|
||||
it('переключение на «Уведомления» показывает матрицу 8×3', async () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
const notifItem = items.find((i) => i.text().includes('Уведомления'));
|
||||
await notifItem!.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('События × каналы');
|
||||
// 8 типов событий из schema users.notification_preferences.
|
||||
['Новый лид', 'Напоминание', 'Низкий баланс', 'Нулевой баланс', 'Анонсы и промо'].forEach((e) =>
|
||||
expect(text).toContain(e),
|
||||
);
|
||||
});
|
||||
|
||||
it('переключение на «Безопасность» показывает 2FA и сессии', async () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
const secItem = items.find((i) => i.text().includes('Безопасность'));
|
||||
await secItem!.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('Двухфакторная авторизация');
|
||||
expect(text).toContain('Активные сессии');
|
||||
});
|
||||
|
||||
it('переключение на «API и Webhook» показывает API-ключ и signing secret', async () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
const apiItem = items.find((i) => i.text().includes('API'));
|
||||
await apiItem!.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('API-ключ');
|
||||
expect(text).toContain('Signing secret');
|
||||
expect(text).toContain('HMAC');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Изменения относительно текущего файла: тест «ровно 8 nav-tabs» → 4; «8 названий вкладок» → 4 рабочих; тест «placeholder-вкладки показывают „В разработке"» удалён, вместо него — регрессия «не содержит placeholder-вкладок».
|
||||
|
||||
- [ ] **Step 2: Прогнать тест — убедиться, что падает**
|
||||
|
||||
Run: `cd app && npm run test:vue -- --run SettingsView`
|
||||
Expected: FAIL — текущий `SettingsView.vue` рендерит 8 вкладок, тесты «ровно 4 nav-tabs» и «не содержит placeholder-вкладок» красные.
|
||||
|
||||
- [ ] **Step 3: Удалить 4 placeholder-вкладки из `SettingsView.vue`**
|
||||
|
||||
Заменить блок `<script setup>` (строки 1–61) на:
|
||||
|
||||
```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 { ref } from 'vue';
|
||||
import ApiTab from './settings/ApiTab.vue';
|
||||
import NotificationsTab from './settings/NotificationsTab.vue';
|
||||
import ProfileTab from './settings/ProfileTab.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: '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');
|
||||
</script>
|
||||
```
|
||||
|
||||
В `<template>` заменить блок `<v-card variant="outlined" class="tab-pane pa-6">…</v-card>` (строки 89–99) на:
|
||||
|
||||
```vue
|
||||
<v-card variant="outlined" class="tab-pane pa-6">
|
||||
<ProfileTab v-if="activeTab === 'profile'" />
|
||||
<SecurityTab v-else-if="activeTab === 'security'" />
|
||||
<ApiTab v-else-if="activeTab === 'api'" />
|
||||
<NotificationsTab v-else-if="activeTab === 'notifications'" />
|
||||
</v-card>
|
||||
```
|
||||
|
||||
`<style scoped>` — без изменений. Удаляются: импорт `PlaceholderTab`, импорт `computed` (становится неиспользуемым — остаётся только `ref`), `placeholderProps` computed, 4 строки placeholder-вкладок в `tabs`, `<PlaceholderTab>` в шаблоне.
|
||||
|
||||
- [ ] **Step 4: Удалить `PlaceholderTab.vue`**
|
||||
|
||||
Удалить файл `app/resources/js/views/settings/PlaceholderTab.vue` (`git rm`). Компонент больше нигде не импортируется (grep `PlaceholderTab` по `app/resources/js` → только `SettingsView.vue`, который мы уже почистили).
|
||||
|
||||
- [ ] **Step 5: Прогнать тест — убедиться, что зелёный**
|
||||
|
||||
Run: `cd app && npm run test:vue -- --run SettingsView`
|
||||
Expected: PASS — все 8 тестов SettingsView зелёные.
|
||||
|
||||
- [ ] **Step 6: Проверить vue-tsc и ESLint**
|
||||
|
||||
Run: `cd app && npm run type-check` → 0 ошибок (важно: неиспользуемый импорт `computed` удалён, иначе vue-tsc/ESLint ругнётся).
|
||||
Run: `cd app && npm run lint:vue` → 0 ошибок.
|
||||
|
||||
- [ ] **Step 7: Полный прогон Vitest (регрессия)**
|
||||
|
||||
Run: `cd app && npm run test:vue`
|
||||
Expected: 0 failed. Базовый объём перед изменением — 100 файлов / 838 passed / 3 skipped; после Sprint 3E удалён 1 тест → ожидается 100 файлов / 837 passed / 3 skipped (точное число — из реального вывода, не экстраполировать).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add app/resources/js/views/SettingsView.vue app/tests/Frontend/SettingsView.spec.ts
|
||||
git rm app/resources/js/views/settings/PlaceholderTab.vue
|
||||
git commit -m "feat(settings): D6/D7 — убрать placeholder-вкладки SettingsView"
|
||||
```
|
||||
|
||||
**НЕ стейджить** `app/dev-indices.json` (авто-генерируемый, pre-existing `M`).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: D6 (4 placeholder-вкладки убраны) ✅; D7 (left-rail 8→4) ✅. «Импорт»-вкладка из D7 — H8/Sprint 4, явно вне scope.
|
||||
- Placeholder scan: нет TODO/TBD; весь код приведён дословно.
|
||||
- Type consistency: `tabs` остаётся `Tab[]`; `activeTab` — `ref('profile')`; `computed` удалён вместе с единственным потребителем `placeholderProps`.
|
||||
Reference in New Issue
Block a user