Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
G7-A — Клиентская «Помощь» (техподдержка) — дизайн
Дата: 2026-06-19
Тип: новая фича (go-live находка G7, часть A)
Источник решения: docs/superpowers/specs/2026-06-18-gfindings-session-handoff.md (строка G7: «НЕ impersonation; вместо — почта техподдержки, клиент оставляет контакты, разбор вручную»).
Декомпозиция: G7 разделён на G7-A (клиентская «Помощь» — этот документ) и G7-B (достройка admin-impersonation — отдельная спека). Делаем обе подряд, G7-A первой.
1. Цель
Дать клиенту в портале простой канал связи с техподдержкой вместо входа-под-клиентом: видимый email техподдержки + форма-заявка (клиент оставляет контакты и сообщение) + чат JivoSite. Заявки уходят на почту техподдержки и сохраняются в БД; разбор — вручную.
Решения владельца (зафиксированы):
- Формат: форма «Оставить заявку» + видимый email и чат JivoSite.
- Заявки: письмо на почту техподдержки + журнал в БД.
- Место: пункт «Помощь» в боковом меню (отдельный экран
/help). - JivoSite: заготовка под конфиг — виджет грузится по ID из env; пока ID не задан, чата нет (не блокирует).
2. Backend
2.1. Конфиг
config/services.php — два ключа:
support.email=env('SUPPORT_EMAIL', 'support@liderra.app')— адрес назначения заявок и для отображения.jivosite.widget_id=env('JIVO_WIDGET_ID')(по умолчаниюnull).
.env.example — добавить SUPPORT_EMAIL=support@liderra.app и JIVO_WIDGET_ID= (пусто).
2.2. Таблица support_requests (RLS по tenant)
Колонки: id BIGSERIAL PK, tenant_id BIGINT NOT NULL (FK tenants, RLS), user_id BIGINT NOT NULL (FK users — кто отправил), name VARCHAR(255) NOT NULL, contact VARCHAR(255) NOT NULL (телефон или email, как ввёл клиент), message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now().
- RLS-политика
tenant_isolation(как у прочих tenant-таблиц). - Индекс
idx_support_requests_tenantна(tenant_id, created_at DESC). - GRANT SELECT, INSERT для
crm_app_user,crm_supplier_worker. - Добавляется в
db/schema.sqlИ в guard'нутую миграцию (CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS,DROP POLICY IF EXISTSпередCREATE POLICY) — по урокам дрейфа схемы. + запись вdb/CHANGELOG_schema.md+ бамп версии schema.sql + header-метрики (таблицы +1, индексы +1, RLS +1). Прогонmigrate:fresh(test) — зелёный, ZERO_DRIFT сохраняется.
2.3. Эндпоинт + модель + письмо
- Модель
App\Models\SupportRequest(с явными@property— ide-helper:models может пропустить, как TenantRequisites; fillable; tenant-scoped). - Роут
POST /api/support-requestsпод['auth:sanctum','tenant']. - Контроллер
Api\SupportRequestController@store: валидацияname|required|string|max:255,contact|required|string|max:255,message|required|string|max:5000. Внутри транзакцииSET LOCAL app.current_tenant_id→SupportRequest::create([...])(основной канал — БД). ЗатемMail::queue(new SupportRequestMail(...))наconfig('services.support.email')в try/catch + Log (без ПДн в логе — только id заявки) — сбой SMTP не валит запрос (паттерн G1sendCode). Ответ201 {ok:true}. - Мейлабл
App\Mail\SupportRequestMail+ шаблонresources/views/emails/support_request.blade.php(имя, контакт, сообщение, тенант, время).
2.4. SPA-shell
resources/views/welcome.blade.php:
<meta name="support-email" content="{{ config('services.support.email') }}">(фронт читает для отображения).- Условная вставка JivoSite-скрипта:
@if(config('services.jivosite.widget_id')) <script src="https://code.jivo.ru/widget/{{ config('services.jivosite.widget_id') }}" async></script> @endif.- Безопасность: SRI (
integrity) к JivoSite неприменим — это динамический widget-loader с меняющимся содержимым,integrityего сломает. Правильный контроль — CSP-allowlist доменаcode.jivo.ru(+ рантайм-домены JivoSite:*.jivosite.com,*.jivo.ru) вscript-src/connect-src/frame-src. На проде CSP правится отдельно при включении виджета (когда владелец задастJIVO_WIDGET_ID); пока id пуст — скрипта нет, CSP не трогаем. Зафиксировать как пункт деплоя.
- Безопасность: SRI (
3. Frontend
- Роут
/help(namehelp, requiresAuth, layout app, devIndex следующий свободный) вrouter/index.ts. - Пункт меню «Помощь» в боковом меню (
components/layout/AppSidebar.vue) — в подходящей группе (напр. «Команда» рядом с «Настройки», или отдельной группой). Иконка Lucide (mdi/lucide help-circle). views/HelpView.vue:- Заголовок «Помощь».
- Блок «Связаться с техподдержкой»: email из
<meta name="support-email">(mailto-ссылка) + подсказка «или напишите в чат» (если JivoSite настроен — он плавающей кнопкой). - Форма «Оставить заявку»: поля Имя / Контакт (телефон или email) / Сообщение (textarea) + кнопка «Отправить». Имя/контакт по умолчанию префиллятся из профиля (
auth.user) — клиенту удобнее. - Сабмит →
POST /api/support-requests. Состояния: загрузка, успех (зелёный алерт «Заявка отправлена, ответим на ваш контакт») с очисткой формы, ошибка (422 — показать под полями; иное — общий алерт).
api/support.ts:submitSupportRequest({name, contact, message}).- Утилита чтения
<meta name="support-email">(или прямо в HelpView).
4. Поток данных
Клиент → «Помощь» (/help) → видит email + форму + (если настроен) чат JivoSite → заполняет форму → POST /api/support-requests → запись в support_requests (БД, RLS) + письмо в очередь на support.email → 201 → клиенту «Заявка отправлена». Поддержка разбирает по почте/в БД вручную.
5. Обработка ошибок
- Валидация: фронт (обязательность) + бэк (422 с полями).
- Сбой почты (SMTP/Unisender) — не теряем заявку: она уже в БД; письмо в очереди с try/catch+Log, запрос не падает.
- JivoSite не настроен (
widget_idпуст) → скрипт не вставляется, чата нет, экран «Помощь» полностью рабочий. - RLS: явный
where(tenant_id)поверхSET LOCAL(как в прочих контроллерах) — заявка привязана к тенанту отправителя.
6. Тестирование
- Backend (Pest):
SupportRequestControllerTest— (1) POST создаёт строку вsupport_requestsс правильным tenant_id/user_id; (2)Mail::assertQueued(SupportRequestMail)на адрес из конфига; (3) валидация 422 при пустых полях; (4) RLS — запись видна только своему тенанту; (5) сбой почты не валит запрос (Mail fake throws → 201 + строка в БД). +migrate:fresh(test) зелёный, ZERO_DRIFT. - Frontend: vitest сломан (G8) →
vue-tsc(0) +eslint(0) +build+ живой Playwright: открыть/help, увидеть email + форму, отправить заявку, увидеть успех; проверить появление строки вsupport_requests(tinker). Меню содержит «Помощь». - Schema:
rls-reviewer-проверка диффа (или ручная — GRANT/policy консистентны, 0 orphan).
Критерий готовности: Pest зелёный, type-check/eslint/build/stan чисты, migrate:fresh зелёный + ZERO_DRIFT, Playwright-сценарий проходит, заявка реально ложится в БД и письмо встаёт в очередь.
7. Вне объёма (G7-A)
- Достройка admin-impersonation — G7-B (отдельная спека, следующей).
- Клиентский список своих заявок (не нужен — разбор вручную).
- Рефактор хардкода
support@liderra.appв страницах ошибок на<meta>— опционально, мелкий follow-up (не ломает G7-A). - Реальный JivoSite widget_id — вставит владелец в env позже.