# Дизайн: перевод блока «Gestión» клиента с Livewire+Alpine на Blade + AJAX

**Тикет:** POL-352
**Дата:** 2026-07-08
**Ветка:** POL-352 (от `refactor/openadmin-separation`)

## Проблема

`App\Livewire\Client\MainActionsComponent` + `resources/views/livewire/main-actions.blade.php` — переусложнённое гибридное решение: Alpine держит полную копию состояния/логики формы (обход графа контактов/результатов, `preformText`, `canSubmit`, таймер, отображение ошибок валидации), при этом сервер отвечает за персистентность, а под-формы SMS/email до сих пор на чистом Livewire (`wire:model.live`). Состояние дублируется (Alpine `actionId` vs Livewire `actionId`), часть кликов дёргает оба (`setAction(3,id); $wire.setAction(3,id)`). Нужно всё максимально упростить до обычного HTML/JS/CSS + AJAX.

## Решение (общая схема)

Убираем Livewire из этого блока. В `resources/views/admin/client/show.blade.php` меняем `@livewire(MainActionsComponent)` на обычный `@include('admin.client.main-actions', [...])`. Компонент и его блейд удаляем. Вместо них:

- **`ClientController@show`** (или тонкий presenter, который он вызывает) собирает те же данные, что раньше считал `mount()` (phones, emails, callSchedules, currentLoan, справочники, JSON-payload `gestionData`, `isCloseCommunication`), и передаёт в партиал. HTML по-прежнему рендерится на сервере.
- **Один `ClientActionsController`** — серверные действия как JSON AJAX-эндпоинты.
- **Один vanilla-JS модуль** заменяет Alpine `gestionForm` и всю обвязку `Livewire.on(...)`.
- **Соседние компоненты остаются на Livewire.** После успешного save/SMS/email JS дёргает `Livewire.dispatch('refreshCommunications')` / `Livewire.dispatch('refreshHeader')` — `CommunicationsComponent` и `HeaderComponent` не трогаем (они слушают эти события: `refreshCommunications` => `$refresh`, `refreshHeader` => `render`).

### Принятые решения (из брейнсторма)

1. **Объём:** только этот блок. Header/Communications остаются Livewire, обновляются через `Livewire.dispatch()` из vanilla JS.
2. **Эндпоинты:** один контроллер `ClientActionsController` с несколькими методами + группа маршрутов в `app/Admin/routes.php`.
3. **Логика формы:** обход графа / preform text / гейтинг сабмита остаются на клиенте (vanilla JS). Сервер перевалидирует на сохранении и возвращает ошибки JSON-ом как источник истины.
4. **Тесты:** PHPUnit feature-тесты на новые эндпоинты (happy + failure).

## Сервер: `ClientActionsController` + `ClientActionsService`

Маршруты в существующую группу `client` в `app/Admin/routes.php` (уже под middleware `admin.panel` / `admin.user.rights` / `password.change`). Формат: `POST admin/client/{clientId}/actions/{verb}`, JSON туда/обратно. CSRF через `<meta name="csrf-token">` (уже в `layouts/admin.blade.php`), заголовок `X-CSRF-TOKEN`.

Единый формат ответа: `{ ok: bool, message?: string, errors?: object, data?: object }`. Ошибки валидации — HTTP 422 с `errors`.

Эндпоинты (источник — методы текущего компонента):

| Эндпоинт | Заменяет | Примечание |
|---|---|---|
| `saveCommunication` | `save()` | Form Request перевалидирует; при ошибке 422 + inline-ошибки |
| `scheduleCall` | `saveCallSchedule()` | |
| `initWebitelCall` | `initWebitelCall()` | возвращает `externalCommunicationId` |
| `sendSms` | `sendSMS()` | возвращает статус + ссылку на bucket |
| `sendEmail` | `sendEmail()` | |
| `smsText` | `updatedSendSmsCompanyId` / `updatedSendSmsCustomOptions` / `setSMSVariable` | заполнение плейсхолдеров через `SmsService` |
| `emailTemplateFields` | `updatedSelectedEmailPlantillas` | извлечение кастом-полей шаблона |
| `emailPreview` | `getEmailPreview()` / `showPreview()` | |
| `addPhone` | `addPhone()` | возвращает перерендеренный HTML партиала телефонов |
| `togglePhoneStatus` | `togglePhoneStatus()` | `{ ok }` |
| `addEmail` | `addEmail()` | возвращает перерендеренный HTML партиала email |
| `toggleEmailStatus` | `toggleEmailStatus()` | `{ ok }` |
| `setChannels` | `setChannels()` | авторизация каналов (normatividad) |

Нетривиальная бизнес-логика из компонента (`buildDtoFromPayload`, `buildCommunicationDto`, `buildPreformText`, `gestionData`, `sortPhones`, оркестрация SMS/email-отправки, правила валидации `rules()`/`messages()`) выносится в **`ClientActionsService`**, чтобы контроллер оставался тонким. Form Request'ы — с массивными правилами (конвенция репо). Правила `saveCommunication` повторяют текущие `rules()`/`messages()` компонента (включая `required_if:contactGroupId,1`, `exclude_unless:showDateInput/showValueInput`, проверку даты «не раньше сегодня»).

## Клиент: один vanilla-JS контроллер

Alpine `gestionForm` переписывается как обычный JS-объект, инициализируемый на `DOMContentLoaded` и привязанный к корню блока. Вся логика формы остаётся на клиенте:

- вкладки (phone/email/another/schedule) и переключение вида список/форма — через `classList`, а не `x-show`;
- обход графа: `availableContacts`, `availableResults`, `contactGroupId`, `showReasonStrategy`, `showValueInput`, `showDateInput`;
- `preformText`, `canSubmit`, `identifier`;
- таймер длительности;
- отрисовка inline-ошибок валидации из JSON-ответа 422.

Читает JSON `gestionData()`, отрендеренный в страницу (через `@js(...)` в data-атрибут или `<script type="application/json">`).

Хелпер `postJson(url, body)` (fetch + `X-CSRF-TOKEN`) заменяет вызовы `$wire.*`. Ответы напрямую управляют существующими SweetAlert-попапами (success/error/info) и `window.open` для ссылок — больше никаких `Livewire.on(...)`. Модалки `addPhone` / `addEmail` / `showNormatividad` остаются (SweetAlert), но постят через AJAX вместо `Livewire.dispatch`.

Файл: `resources/js/client-actions.js`, сборка через Vite. Инициализация без jQuery для новой логики (jQuery остаётся допустим там, где уже используется в проекте — маски телефонов IMask, flatpickr).

## Перерисовка таблиц после мутаций

Сейчас Livewire перерисовывает таблицы телефонов/email после add/toggle (компонент пушит в коллекцию и ре-рендерит). Чтобы не дублировать разметку строк в JS: эндпоинты `addPhone` / `addEmail` **возвращают заново отрендеренный HTML партиала** `admin/client/phones` / `admin/client/emails`, JS подменяет `innerHTML` контейнера таблицы. Разметка остаётся в Blade. `togglePhoneStatus` / `toggleEmailStatus` возвращают `{ ok }` (чекбокс уже отражает состояние).

## Файлы

**Удалить:**
- `app/Livewire/Client/MainActionsComponent.php`
- `resources/views/livewire/main-actions.blade.php`

**Перенести + убрать Livewire** (заменить `$this->…` / `wire:` / `x-…` на переданные переменные + JS-хуки), в `resources/views/admin/client/…`:
- `livewire/actions-wrapper.blade.php`
- `livewire/client/phones.blade.php`
- `livewire/client/emails.blade.php`
- `livewire/client/gestion-form.blade.php`
- партиалы модалок add-phone / add-email и `livewire/normatividad.blade.php`

**Добавить:**
- `app/Admin/Controllers/Client/ClientActionsController.php`
- `app/Services/Client/ClientActionsService.php` (или ближайшее по конвенции место)
- Form Request(ы) под `app/Http/Requests/Client/…`
- `resources/js/client-actions.js` (+ регистрация во Vite)
- новый корневой партиал `resources/views/admin/client/main-actions.blade.php`

**Изменить:**
- `app/Admin/Controllers/Client/ClientController@show` — сбор и передача данных (перенос логики `mount()`)
- `app/Admin/routes.php` — маршруты `{clientId}/actions/*`
- `resources/views/admin/client/show.blade.php` — `@livewire` → `@include`

## Тесты

PHPUnit feature-тесты на каждый эндпоинт (happy + failure) в `tests/Feature/Admin/Client/`. Т.к. в репо тестов почти нет, сюда входит поднятие admin-авторизации + сидинг справочников (actions/contacts/results/code_relations/strategies/non_payment_reasons) в setup. Покрыть как минимум:
- `saveCommunication`: успех; 422 при отсутствии `resultId`; `required_if` для reason/strategy при `contactGroupId=1`; правило даты «не раньше сегодня».
- `scheduleCall`: успех; ошибка при пустой дате; конфликт (дубль даты).
- `sendSms` / `sendEmail`: ветки статусов WAITING / FAILED / успех; блок при нерабочем дне (`Calendar::isTodayNonWorking`).
- `addPhone` / `addEmail` / `togglePhoneStatus` / `toggleEmailStatus` / `setChannels`: успех + базовая валидация.

## Риски

- **(а)** Перенос загрузки данных из `mount()` в контроллер/presenter — самый крупный кусок; нужно точно перенести справочники и флаги (`isCloseCommunication` через `ReadLoansRepository`/`CasaHelper`).
- **(б)** JS-переписка должна точно повторить логику графа/валидации/таймера, иначе ломается сохранение gestión. Поведение сохраняется один-в-один, меняется только «проводка».
- **(в)** Соседние Livewire-компоненты должны корректно ловить `Livewire.dispatch()` из vanilla JS (Livewire 3 это поддерживает) — проверить на реальной странице.

## Вне объёма

- Конвертация `HeaderComponent` / `CommunicationsComponent` / прочих Livewire-компонентов страницы.
- Изменение бизнес-логики отправки SMS/email/сохранения gestión — только перенос, без изменения поведения.