Files
portal/docs/superpowers/specs/2026-06-19-g7a-client-support-design.md
T
2026-06-19 14:18:16 +03:00

10 KiB
Raw Blame History

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_idSupportRequest::create([...]) (основной канал — БД). Затем Mail::queue(new SupportRequestMail(...)) на config('services.support.email') в try/catch + Log (без ПДн в логе — только id заявки) — сбой SMTP не валит запрос (паттерн G1 sendCode). Ответ 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 не трогаем. Зафиксировать как пункт деплоя.

3. Frontend

  • Роут /help (name help, 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 позже.