# G7-A — Клиентская «Помощь» — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Дать клиенту экран «Помощь» с email техподдержки + формой-заявкой (→ запись в БД `support_requests` + письмо в поддержку) + заготовкой JivoSite (config-driven).

**Architecture:** Backend — конфиг (`services.php`) + tenant-RLS таблица `support_requests` (schema.sql + guard'нутая миграция) + `POST /api/support-requests` (store в БД, затем `Mail::queue` в try/catch). Frontend — роут `/help` + пункт «Помощь» в сайдбаре + `HelpView.vue` (форма). JivoSite — условный `<script>` в shell-blade по env-ID. Спека: `docs/superpowers/specs/2026-06-19-g7a-client-support-design.md`.

**Tech Stack:** Laravel 13 / Pest 4 / PostgreSQL 16 (RLS, partitioning conventions); Vue 3 + Vuetify 3 / vue-tsc / eslint / Vite. Unisender Go (SMTP, очередь).

**Окружение (факты):**
- Pest: `composer --working-dir=app test -- <path>`. Pint: `composer --working-dir=app pint <files>`.
- Миграции: dev `php app/artisan migrate --force`; тест `DB_DATABASE=liderra_testing php app/artisan migrate --force`. Полная пересборка теста: `DB_DATABASE=liderra_testing php app/artisan migrate:fresh --force`.
- Фронт: `npm --prefix app run type-check` / `npm --prefix app run lint:vue` / `npm --prefix app run build`. **vitest сломан (G8)** → фронт-юнитов нет, верификация = type-check+eslint+build+живой Playwright.
- Коммит: `LEFTHOOK=0 git commit -m "..." -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"`. Push: `git push gitea main`.
- ⚠ Репозиторий имеет ПРЕД-существующие грязные файлы (CLAUDE.md, .claude/settings.json, observer, CTemp*.txt) — НЕ трогать/не стейджить. Стейджить ТОЛЬКО явные пути, НИКОГДА `git add -A`.
- Схема — источник истины: новые таблицы в `db/schema.sql` И в guard'нутую миграцию; после — `migrate:fresh` (test) зелёный + ZERO_DRIFT (0 таблиц БД отсутствуют в schema.sql).

---

## Файловая карта

**Backend (create):** `app/app/Models/SupportRequest.php`, `app/app/Http/Controllers/Api/SupportRequestController.php`, `app/app/Mail/SupportRequestMail.php`, `app/resources/views/emails/support_request.blade.php`, `app/database/migrations/2026_06_19_140000_create_support_requests.php`, `app/tests/Feature/Support/SupportRequestControllerTest.php`.
**Backend (modify):** `app/config/services.php`, `app/.env.example`, `app/routes/web.php`, `db/schema.sql`, `db/CHANGELOG_schema.md`, `app/resources/views/welcome.blade.php`.
**Frontend (create):** `app/resources/js/views/HelpView.vue`, `app/resources/js/api/support.ts`.
**Frontend (modify):** `app/resources/js/router/index.ts`, `app/resources/js/components/layout/AppSidebar.vue`.

---

## Task 1: Backend config (support email + JivoSite)

**Files:** Modify `app/config/services.php`, `app/.env.example`.

- [ ] **Step 1: Add config keys**

В `app/config/services.php` перед закрывающим `];` добавить:
```php
    // G7-A: клиентская «Помощь».
    'support' => [
        'email' => env('SUPPORT_EMAIL', 'support@liderra.app'),
    ],
    'jivosite' => [
        'widget_id' => env('JIVO_WIDGET_ID'),
    ],
```

- [ ] **Step 2: .env.example**

В `app/.env.example` добавить (рядом с MAIL_*):
```
SUPPORT_EMAIL=support@liderra.app
JIVO_WIDGET_ID=
```

- [ ] **Step 3: Verify config loads**

Run: `php app/artisan config:clear && php app/artisan tinker --execute="echo config('services.support.email').'|'.(config('services.jivosite.widget_id') ?? 'null');"`
Expected: `support@liderra.app|null`.

- [ ] **Step 4: Commit**
```bash
git add app/config/services.php app/.env.example
LEFTHOOK=0 git commit -m "feat(G7-A): конфиг support.email + jivosite.widget_id" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 2: Таблица support_requests (schema.sql + миграция)

**Files:** Create `app/database/migrations/2026_06_19_140000_create_support_requests.php`; Modify `db/schema.sql`, `db/CHANGELOG_schema.md`.

- [ ] **Step 1: Migration (guarded, pgsql_supplier — паттерн tenant_requisites)**

Create `app/database/migrations/2026_06_19_140000_create_support_requests.php`:
```php
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up(): void
    {
        $supplier = DB::connection('pgsql_supplier');

        $supplier->statement(<<<'SQL'
            CREATE TABLE IF NOT EXISTS support_requests (
                id         BIGSERIAL PRIMARY KEY,
                tenant_id  BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
                user_id    BIGINT NOT NULL REFERENCES users(id),
                name       VARCHAR(255) NOT NULL,
                contact    VARCHAR(255) NOT NULL,
                message    TEXT NOT NULL,
                created_at TIMESTAMPTZ NOT NULL DEFAULT now()
            )
        SQL);
        $supplier->statement('CREATE INDEX IF NOT EXISTS idx_support_requests_tenant ON support_requests (tenant_id, created_at DESC)');
        $supplier->statement('ALTER TABLE support_requests ENABLE ROW LEVEL SECURITY');
        // PG не поддерживает CREATE POLICY IF NOT EXISTS → дроп-перед-создание (идемпотентность к schema.sql).
        $supplier->statement('DROP POLICY IF EXISTS support_requests_tenant_isolation ON support_requests');
        $supplier->statement(<<<'SQL'
            CREATE POLICY support_requests_tenant_isolation ON support_requests
                USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint)
        SQL);
        foreach (['crm_app_user', 'crm_supplier_worker'] as $role) {
            $supplier->statement(<<<SQL
                DO \$\$
                BEGIN
                    IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{$role}') THEN
                        GRANT SELECT, INSERT ON support_requests TO {$role};
                        GRANT USAGE, SELECT ON SEQUENCE support_requests_id_seq TO {$role};
                    END IF;
                END
                \$\$
            SQL);
        }
    }

    public function down(): void
    {
        DB::connection('pgsql_supplier')->statement('DROP TABLE IF EXISTS support_requests CASCADE');
    }
};
```

- [ ] **Step 2: Add the table to db/schema.sql**

В `db/schema.sql`, в логичной секции (рядом с другими tenant-RLS клиентскими таблицами, напр. возле `tenant_requisites`), добавить:
```sql
CREATE TABLE support_requests (
    id         BIGSERIAL PRIMARY KEY,
    tenant_id  BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    user_id    BIGINT NOT NULL REFERENCES users(id),
    name       VARCHAR(255) NOT NULL,
    contact    VARCHAR(255) NOT NULL,
    message    TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_support_requests_tenant ON support_requests (tenant_id, created_at DESC);
ALTER TABLE support_requests ENABLE ROW LEVEL SECURITY;
CREATE POLICY support_requests_tenant_isolation ON support_requests
    USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint);
GRANT SELECT, INSERT ON support_requests TO crm_app_user, crm_supplier_worker;
GRANT USAGE, SELECT ON SEQUENCE support_requests_id_seq TO crm_app_user, crm_supplier_worker;
COMMENT ON TABLE support_requests IS 'Заявки клиента в техподдержку (G7-A). RLS по tenant_id; разбор вручную (письмо + журнал).';
```
Match the file's indentation/comment style. Place near other tenant-RLS tables.

- [ ] **Step 3: Header metrics + version**

В header `db/schema.sql`: tables +1 (regular), indexes +1, RLS policies +1. Бамп версии (читать текущую, напр. v8.47 → v8.48).

- [ ] **Step 4: CHANGELOG_schema.md**

Добавить top-запись: версия, дата 2026-06-19, «G7-A: таблица `support_requests` (заявки клиента в техподдержку) — RLS tenant_isolation, индекс, GRANTs. Миграция 2026_06_19_140000 (guarded).»

- [ ] **Step 5: Apply on dev + test, verify migrate:fresh + ZERO_DRIFT**

Run:
```bash
php app/artisan migrate --force
DB_DATABASE=liderra_testing php app/artisan migrate:fresh --force 2>&1 | tail -5
```
Expected: всё DONE, без «already exists». Затем ZERO_DRIFT:
```bash
DB_DATABASE=liderra_testing php app/artisan tinker --execute="\$db=collect(DB::select(\"SELECT tablename FROM pg_tables WHERE schemaname='public'\"))->pluck('tablename'); \$s=file_get_contents(dirname(base_path()).'/db/schema.sql'); \$m=[]; foreach(\$db as \$t){ if(preg_match('/_y20\\d\\d_m\\d\\d\$/',\$t))continue; if(in_array(\$t,['migrations','jobs','job_batches','failed_jobs','cache','cache_locks','sessions','password_reset_tokens']))continue; if(!preg_match('/CREATE TABLE '.\$t.'\\b/',\$s))\$m[]=\$t; } echo empty(\$m)?'ZERO_DRIFT':('STILL_MISSING: '.implode(',',\$m));"
```
Expected: `ZERO_DRIFT`. И `grep -cE "CREATE TABLE support_requests\b" db/schema.sql` → 1.

- [ ] **Step 6: Commit**
```bash
git add app/database/migrations/2026_06_19_140000_create_support_requests.php db/schema.sql db/CHANGELOG_schema.md
LEFTHOOK=0 git commit -m "feat(G7-A): таблица support_requests (schema + миграция, RLS)" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 3: Модель SupportRequest

**Files:** Create `app/app/Models/SupportRequest.php`.

- [ ] **Step 1: Model (явные @property — ide-helper:models может пропустить)**

Create `app/app/Models/SupportRequest.php`:
```php
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;

/**
 * Заявка клиента в техподдержку (G7-A). RLS по tenant_id; created_at — DB default.
 *
 * @property int $id
 * @property int $tenant_id
 * @property int $user_id
 * @property string $name
 * @property string $contact
 * @property string $message
 * @property Carbon $created_at
 */
class SupportRequest extends Model
{
    protected $table = 'support_requests';

    public $timestamps = false; // только created_at (DB DEFAULT now()), updated_at нет

    protected $fillable = [
        'tenant_id', 'user_id', 'name', 'contact', 'message',
    ];

    protected function casts(): array
    {
        return ['created_at' => 'datetime'];
    }
}
```

- [ ] **Step 2: Commit**
```bash
composer --working-dir=app pint app/Models/SupportRequest.php
git add app/app/Models/SupportRequest.php
LEFTHOOK=0 git commit -m "feat(G7-A): модель SupportRequest" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 4: Mailable + шаблон письма

**Files:** Create `app/app/Mail/SupportRequestMail.php`, `app/resources/views/emails/support_request.blade.php`.

- [ ] **Step 1: Mailable (паттерн NewLeadsDigestMail)**

Create `app/app/Mail/SupportRequestMail.php`:
```php
<?php

declare(strict_types=1);

namespace App\Mail;

use App\Models\SupportRequest;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

/**
 * Письмо в техподдержку о новой заявке клиента (G7-A). Адресат — config('services.support.email').
 */
class SupportRequestMail extends Mailable
{
    use Queueable;
    use SerializesModels;

    public function __construct(public SupportRequest $request) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Лидерра. Заявка в поддержку #'.$this->request->id,
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.support_request',
            with: ['r' => $this->request],
        );
    }
}
```

- [ ] **Step 2: Email blade**

Create `app/resources/views/emails/support_request.blade.php`:
```blade
<p>Новая заявка в техподдержку Лидерры.</p>
<ul>
    <li><strong>Заявка #:</strong> {{ $r->id }}</li>
    <li><strong>Тенант:</strong> {{ $r->tenant_id }}</li>
    <li><strong>Пользователь:</strong> {{ $r->user_id }}</li>
    <li><strong>Имя:</strong> {{ $r->name }}</li>
    <li><strong>Контакт:</strong> {{ $r->contact }}</li>
    <li><strong>Время:</strong> {{ $r->created_at?->format('d.m.Y H:i') }}</li>
</ul>
<p><strong>Сообщение:</strong></p>
<p>{{ $r->message }}</p>
```

- [ ] **Step 3: Commit**
```bash
composer --working-dir=app pint app/Mail/SupportRequestMail.php
git add app/app/Mail/SupportRequestMail.php app/resources/views/emails/support_request.blade.php
LEFTHOOK=0 git commit -m "feat(G7-A): SupportRequestMail + шаблон письма" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 5: Контроллер + роут (TDD)

**Files:** Create `app/app/Http/Controllers/Api/SupportRequestController.php`, `app/tests/Feature/Support/SupportRequestControllerTest.php`; Modify `app/routes/web.php`.

- [ ] **Step 1: Write failing test**

Create `app/tests/Feature/Support/SupportRequestControllerTest.php`:
```php
<?php

declare(strict_types=1);

use App\Mail\SupportRequestMail;
use App\Models\SupportRequest;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Support\Facades\Mail;

use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;

beforeEach(function () {
    Mail::fake();
    $this->tenant = Tenant::factory()->create();
    $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
});

it('создаёт заявку в БД и ставит письмо в очередь', function () {
    actingAs($this->user);

    postJson('/api/support-requests', [
        'name' => 'Иван',
        'contact' => '+79991234567',
        'message' => 'Не приходят лиды.',
    ])->assertCreated()->assertJson(['ok' => true]);

    $row = SupportRequest::query()->where('tenant_id', $this->tenant->id)->first();
    expect($row)->not->toBeNull()
        ->and($row->user_id)->toBe($this->user->id)
        ->and($row->name)->toBe('Иван')
        ->and($row->contact)->toBe('+79991234567')
        ->and($row->message)->toBe('Не приходят лиды.');

    Mail::assertQueued(SupportRequestMail::class, fn (SupportRequestMail $m) => $m->hasTo(config('services.support.email')));
});

it('валидирует обязательные поля (422)', function () {
    actingAs($this->user);
    postJson('/api/support-requests', ['name' => '', 'contact' => '', 'message' => ''])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['name', 'contact', 'message']);
});

it('не теряет заявку при сбое почты', function () {
    actingAs($this->user);
    Mail::shouldReceive('queue')->andThrow(new RuntimeException('smtp down'));

    postJson('/api/support-requests', [
        'name' => 'Пётр', 'contact' => 'p@example.org', 'message' => 'Вопрос.',
    ])->assertCreated();

    expect(SupportRequest::query()->where('tenant_id', $this->tenant->id)->count())->toBe(1);
});
```

- [ ] **Step 2: Run — verify it fails (no route/controller)**

Run: `composer --working-dir=app test -- tests/Feature/Support/SupportRequestControllerTest.php`
Expected: FAIL (404 / route not defined).

- [ ] **Step 3: Controller**

Create `app/app/Http/Controllers/Api/SupportRequestController.php`:
```php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Mail\SupportRequestMail;
use App\Models\SupportRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;

/**
 * G7-A: приём клиентских заявок в техподдержку. Запись в БД — основной канал;
 * письмо в поддержку — best-effort (сбой SMTP не валит запрос, паттерн G1 sendCode).
 */
class SupportRequestController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'contact' => 'required|string|max:255',
            'message' => 'required|string|max:5000',
        ]);

        /** @var User $user */
        $user = $request->user();

        $supportRequest = DB::transaction(function () use ($user, $validated): SupportRequest {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            return SupportRequest::create([
                'tenant_id' => $user->tenant_id,
                'user_id' => $user->id,
                'name' => $validated['name'],
                'contact' => $validated['contact'],
                'message' => $validated['message'],
            ]);
        });

        // Письмо — best-effort: заявка уже в БД, сбой почты не теряет её и не валит запрос.
        try {
            Mail::to(config('services.support.email'))->queue(new SupportRequestMail($supportRequest));
        } catch (\Throwable $e) {
            Log::warning('SupportRequestMail queue failed', ['id' => $supportRequest->id, 'error' => $e->getMessage()]);
        }

        return response()->json(['ok' => true], 201);
    }
}
```

- [ ] **Step 4: Route**

В `app/routes/web.php` добавить (рядом с прочими `auth:sanctum`+`tenant` API-группами):
```php
// G7-A: клиентские заявки в техподдержку.
Route::middleware(['auth:sanctum', 'tenant'])->post('/api/support-requests', 'App\Http\Controllers\Api\SupportRequestController@store');
```

- [ ] **Step 5: Run — verify pass**

Run: `composer --working-dir=app test -- tests/Feature/Support/SupportRequestControllerTest.php`
Expected: 3 passed.

- [ ] **Step 6: Commit**
```bash
composer --working-dir=app pint app/Http/Controllers/Api/SupportRequestController.php
git add app/app/Http/Controllers/Api/SupportRequestController.php app/routes/web.php app/tests/Feature/Support/SupportRequestControllerTest.php
LEFTHOOK=0 git commit -m "feat(G7-A): POST /api/support-requests + тест (store+mail+валидация)" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 6: SPA-shell (meta support-email + JivoSite)

**Files:** Modify `app/resources/views/welcome.blade.php`.

- [ ] **Step 1: Inject meta + conditional JivoSite**

Заменить `<head>...</head>` блок в `app/resources/views/welcome.blade.php`, добавив после `<title>`:
```blade
    <meta name="support-email" content="{{ config('services.support.email') }}">
    @vite(['resources/css/app.css', 'resources/js/app.ts'])
    @if(config('services.jivosite.widget_id'))
        <script src="https://code.jivo.ru/widget/{{ config('services.jivosite.widget_id') }}" async></script>
    @endif
</head>
```
(итог: `<title>` → `<meta support-email>` → `@vite` → условный JivoSite → `</head>`). NB: SRI к JivoSite неприменим (динамический loader); контроль — CSP-allowlist `code.jivo.ru` при включении на проде (пункт деплоя, пока id пуст — скрипта нет).

- [ ] **Step 2: Verify blade renders (id пуст → нет скрипта)**

Run: `php app/artisan view:clear && curl -s http://127.0.0.1:8000/login | grep -c "support-email"` (если сервер запущен; иначе пропустить — проверится в Playwright).
Expected: `1` (meta есть), и нет `code.jivo.ru` (id пуст).

- [ ] **Step 3: Commit**
```bash
git add app/resources/views/welcome.blade.php
LEFTHOOK=0 git commit -m "feat(G7-A): meta support-email + условный JivoSite в shell-blade" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 7: Фронт — роут /help + пункт «Помощь» в меню

**Files:** Modify `app/resources/js/router/index.ts`, `app/resources/js/components/layout/AppSidebar.vue`.

- [ ] **Step 1: Route**

В `app/resources/js/router/index.ts` добавить (рядом с прочими app-роутами, напр. после `/settings`):
```ts
    {
        path: '/help',
        name: 'help',
        component: () => import('../views/HelpView.vue'),
        meta: {
            layout: 'app',
            title: 'Помощь',
            requiresAuth: true,
            transition: 'ld-route-fadeup',
            devIndex: 33,
            devLabel: 'Помощь',
        },
    },
```
(devIndex 33 — следующий свободный; если 33 занят, взять следующий незанятый.)

- [ ] **Step 2: Nav item «Помощь»**

В `app/resources/js/components/layout/AppSidebar.vue`, в `navGroups`, в группу `'Команда'` добавить пункт после «Настройки»:
```ts
    {
        eyebrow: 'Команда',
        items: [
            { title: 'Настройки', icon: 'mdi-cog-outline', to: '/settings' },
            { title: 'Помощь', icon: 'mdi-lifebuoy', to: '/help' },
        ],
    },
```

- [ ] **Step 3: type-check (упадёт — HelpView ещё нет; это ок до Task 8)**

Пропустить прогон до Task 8 (HelpView создаётся там). Коммит — в конце Task 8.

---

## Task 8: Фронт — HelpView + api/support.ts

**Files:** Create `app/resources/js/views/HelpView.vue`, `app/resources/js/api/support.ts`.

- [ ] **Step 1: api/support.ts**

Create `app/resources/js/api/support.ts`:
```ts
import { apiClient } from './client';

export interface SupportRequestPayload {
    name: string;
    contact: string;
    message: string;
}

export async function submitSupportRequest(payload: SupportRequestPayload): Promise<void> {
    await apiClient.post('/api/support-requests', payload);
}
```
NB: проверить имя экспортируемого axios-инстанса в `app/resources/js/api/client.ts` (grep `export`); если он называется иначе (напр. `api` / default), импортировать соответствующе.

- [ ] **Step 2: HelpView.vue**

Create `app/resources/js/views/HelpView.vue`:
```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '../stores/auth';
import { submitSupportRequest } from '../api/support';

const auth = useAuthStore();

const supportEmail =
    document.querySelector('meta[name="support-email"]')?.getAttribute('content') ?? 'support@liderra.app';

const name = ref(
    [auth.user?.first_name, auth.user?.last_name].filter(Boolean).join(' ') || '',
);
const contact = ref(auth.user?.email ?? '');
const message = ref('');
const loading = ref(false);
const sent = ref(false);
const errorMsg = ref('');
const fieldErrors = ref<Record<string, string[]>>({});

async function submit() {
    errorMsg.value = '';
    fieldErrors.value = {};
    if (!name.value.trim() || !contact.value.trim() || !message.value.trim()) {
        errorMsg.value = 'Заполните все поля.';
        return;
    }
    loading.value = true;
    try {
        await submitSupportRequest({ name: name.value, contact: contact.value, message: message.value });
        sent.value = true;
        message.value = '';
    } catch (e: unknown) {
        const err = e as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } };
        if (err.response?.status === 422 && err.response.data?.errors) {
            fieldErrors.value = err.response.data.errors;
        } else {
            errorMsg.value = 'Не удалось отправить. Попробуйте ещё раз или напишите на почту.';
        }
    } finally {
        loading.value = false;
    }
}
</script>

<template>
    <div class="help-view pa-6" data-testid="help-view">
        <h1 class="text-h5 mb-1">Помощь</h1>
        <p class="text-body-2 text-medium-emphasis mb-6">
            Напишите нам — ответим на ваш контакт. Можно по почте, через форму ниже или в чат справа.
        </p>

        <v-card variant="outlined" class="pa-5 mb-4" max-width="640">
            <h3 class="text-subtitle-2 mb-2">Почта техподдержки</h3>
            <a :href="`mailto:${supportEmail}`" class="text-primary" data-testid="support-email">{{ supportEmail }}</a>
        </v-card>

        <v-card variant="outlined" class="pa-5" max-width="640">
            <h3 class="text-subtitle-2 mb-3">Оставить заявку</h3>

            <v-alert v-if="sent" type="success" variant="tonal" class="mb-4" data-testid="support-sent">
                Заявка отправлена. Мы свяжемся с вами по указанному контакту.
            </v-alert>
            <v-alert v-if="errorMsg" type="error" variant="tonal" class="mb-4">{{ errorMsg }}</v-alert>

            <v-text-field
                v-model="name"
                label="Имя"
                :error-messages="fieldErrors.name"
                density="comfortable"
                class="mb-2"
                data-testid="support-name"
            />
            <v-text-field
                v-model="contact"
                label="Контакт (телефон или email)"
                :error-messages="fieldErrors.contact"
                density="comfortable"
                class="mb-2"
                data-testid="support-contact"
            />
            <v-textarea
                v-model="message"
                label="Сообщение"
                :error-messages="fieldErrors.message"
                rows="4"
                density="comfortable"
                class="mb-3"
                data-testid="support-message"
            />

            <v-btn color="primary" :loading="loading" data-testid="support-submit" @click="submit">Отправить</v-btn>
        </v-card>
    </div>
</template>
```
NB: проверить, что `auth.user` имеет поля `first_name`/`last_name`/`email` (grep по `stores/auth.ts` / типу User); если имена иные — подставить фактические.

- [ ] **Step 3: type-check + eslint + build**

Run: `npm --prefix app run type-check && npm --prefix app run lint:vue && npm --prefix app run build`
Expected: 0 ошибок, build OK.

- [ ] **Step 4: Commit (роут+меню+экран вместе)**
```bash
git add app/resources/js/router/index.ts app/resources/js/components/layout/AppSidebar.vue app/resources/js/views/HelpView.vue app/resources/js/api/support.ts
LEFTHOOK=0 git commit -m "feat(G7-A): экран «Помощь» (форма-заявка) + пункт меню + роут" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Task 9: Верификация

**Files:** только прогон.

- [ ] **Step 1: Backend Pest + adjacent**

Run: `composer --working-dir=app test -- tests/Feature/Support tests/Feature/Auth/NotificationPreferencesTest.php`
Expected: всё PASS (новый тест 3/3 + смежный регресс).

- [ ] **Step 2: stan + type-check + eslint + build**

Run:
```bash
composer --working-dir=app stan
npm --prefix app run type-check
npm --prefix app run lint:vue
npm --prefix app run build
```
Expected: stan 0 (если новые ложноположительные Pest-методы в SupportRequestControllerTest — добавить в phpstan-baseline тем же паттерном, что прочие тесты; см. как в проекте baseline'ятся `TestCall::postJson`), type-check/eslint 0, build OK.

- [ ] **Step 3: migrate:fresh + ZERO_DRIFT (повтор после всех правок)**

Run: `DB_DATABASE=liderra_testing php app/artisan migrate:fresh --force 2>&1 | tail -3` + ZERO_DRIFT-скан из Task 2 Step 5.
Expected: зелёный + `ZERO_DRIFT`.

- [ ] **Step 4: Живой Playwright**

Поднять сервер (`php app/artisan serve`), пересобрать ассеты (`npm --prefix app run build`), залогиниться демо-клиентом (`admin@demo.local` / `password`, либо создать через самозапись). Через Playwright MCP на `127.0.0.1:8000`:
- (а) в боковом меню есть пункт «Помощь»; клик → `/help`.
- (б) экран «Помощь»: виден email техподдержки (`data-testid="support-email"`), форма (имя/контакт/сообщение).
- (в) заполнить сообщение, нажать «Отправить» → зелёный алерт `data-testid="support-sent"`; консоль без ошибок.
- (г) проверить запись в БД: `php app/artisan tinker --execute="echo App\Models\SupportRequest::count();"` ≥ 1.
- (д) JivoSite: id пуст → чата нет, экран рабочий (нет JS-ошибок про jivo).

- [ ] **Step 5: Финальный grep — нет хвостов**

Run: `grep -rin "support_requests\|SupportRequest" app/app | grep -vi "test"` — ожидается: модель/контроллер/мейлабл/миграция (живые ссылки, без мусора).

---

## Task 10: Push + память

- [ ] **Step 1: Push**
```bash
git push gitea main
```
Expected: pre-push (gitleaks + lychee) проходит.

- [ ] **Step 2: Память**

Обновить `project-golive-findings-status.md`: G7-A DONE (дата, что осталось — G7-B impersonation). Требует `coverage: direct:memory-sync` или фразу `memory dump`.

---

## Self-Review плана (выполнено автором)

- **Покрытие спеки:** §2.1 конфиг → Task 1; §2.2 таблица → Task 2; §2.3 модель/эндпоинт/письмо → Task 3/4/5; §2.4 shell → Task 6; §3 фронт → Task 7/8; §4 поток → реализован Task 5+8; §5 ошибки → Task 5 (try/catch, 422) + Task 8 (UI-состояния); §6 тесты → Task 5 (TDD) + Task 9. Пробелов нет.
- **Плейсхолдеры:** нет TBD/«добавить валидацию» без кода — весь код приведён. NB-пометки (проверить имя axios-инстанса / поля User / devIndex) — это явные verify-инструкции, не пропуски.
- **Согласованность имён:** таблица `support_requests` / модель `SupportRequest` / политика `support_requests_tenant_isolation` / эндпоинт `POST /api/support-requests` / `config('services.support.email')` / `config('services.jivosite.widget_id')` / `meta[name=support-email]` / `submitSupportRequest` — единообразно во всех задачах.
