Импорт сотрудников из PayKeeper — wizard

Источник требований

Pull-импорт существующих кассиров из ЛК PayKeeper в наш ERP. Запускается по требованию владельца — кнопкой в разделе «Сотрудники». PK даёт минимум полей (id, login, email, fio, admin, refund, invoices_only), остальные обязательные у нас (first_name, last_name, password, phone, pin, roles, stores) дозаполняет владелец в wizard’е.


1. Кнопка «Выгрузить из PK» в /employees

Где расположена

В шапке списка сотрудников /employees рядом с кнопкой «Добавить сотрудника». Также рядом — кнопка «Журнал импортов» (см. §4).

Видимость и активность

УсловиеПоведение
Нет integrations.readКнопка скрыта
Есть integrations.read, но нет integrations.manage ИЛИ нет employees.editКнопка видна, но disabled, tooltip «Недостаточно прав»
Нет ни одного active PK-аккаунта во scopeDisabled, tooltip «Сначала подключите PayKeeper в карточке ЮЛ»
Все PK-аккаунты suspendedDisabled, tooltip «Все интеграции PayKeeper приостановлены»
Permissions есть AND ≥1 active PK-аккаунтАктивна

Поток при клике

  1. Загрузить GET /api/v1/admin/paykeeper/accounts?status=active (отфильтровать по scope)
  2. Если 1 ЛК → сразу переход на /employees/import-from-paykeeper?account_id={id} (Шаг 1 wizard’а)
  3. Если >1 ЛК → сначала модалка выбора (см. §2)
  4. Если 0 active (race condition с шагом загрузки кнопки) → toast «Нет активных интеграций PayKeeper»

2. Модалка выбора ЛК PK (только при >1)

┌─ Выберите ЛК PayKeeper для импорта ────────────────┐
│                                                    │
│  ○ ООО «Ромашка»                                   │
│    example.server.paykeeper.ru                     │
│                                                    │
│  ○ ИП Петров                                       │
│    petrov.server.paykeeper.ru                      │
│                                                    │
│  Один импорт — для одного ЛК.                      │
│  Чтобы импортировать из другого ЛК — повторите     │
│  процедуру.                                        │
│                                                    │
│  [Отмена]                       [Продолжить →]     │
└────────────────────────────────────────────────────┘
  • Имя ЮЛ + хост ЛК — для каждого аккаунта
  • Per-account preview не делаем (не дёргаем PK заранее — это лишний трафик)
  • Submit → переход на /employees/import-from-paykeeper?account_id={selected}

3. Wizard — 3 шага

Реализован как отдельная страница /employees/import-from-paykeeper?account_id={id}. Преимущества по сравнению с модалкой: URL для возврата по back, удобнее работать с большим списком кандидатов.

В шапке страницы:

  • Crumbs: Сотрудники / Импорт из PayKeeper
  • Под crumbs — индикатор шагов: ① Кандидаты ──── ② Дозаполнение ──── ③ Результат
  • Имя ЮЛ + хост ЛК PK — справа в шапке для контекста

Шаг 1: Список кандидатов

Загрузка

  • API: POST /api/v1/admin/paykeeper/accounts/{id}/employees/preview
  • Состояние loading: skeleton-таблица + текст «Получаем список из PayKeeper…»
  • Ошибки:
    • ACCOUNT_NOT_ACTIVE (422) → блокирующий блок «Интеграция PayKeeper приостановлена. Возобновите в карточке ЮЛ для запуска импорта.» + кнопка «Закрыть»
    • PK_CONNECTION_FAILED (422) → блок «Не удалось получить список из PayKeeper: {msg}» + кнопки «Повторить» / «Закрыть»
    • Empty list (PK вернул []) → блок «В этом ЛК PayKeeper нет пользователей. Заведите кассиров в ЛК PayKeeper → Настройки → Доступ к панели администратора, затем повторите импорт.» + кнопка «Закрыть»

Таблица кандидатов

Логин PKEmailФИОAdminСтатус матчаДействие
admin системаspad20@yandex.ru🆕 Новый[Создать ▼]
etet@paykeeper.ruETA🟡 Совпадает email[Связать ▼]
koala🆕 Новый[Создать ▼]
user система🔗 Уже импортирован(disabled)

Чекбокс — для bulk-actions (выбрать всё / снять выделение / применить одно действие к выбранным). На MVP — оставить, реализация bulk-actions опционально (можно сделать как отдельный кнопочный ряд «Применить ко всем выбранным: [действие ▼]»).

Бейдж «система» — для пользователей с pk_login равным admin / user (системные технические аккаунты PK). Tooltip: «Системный пользователь PayKeeper. Обычно это технический аккаунт ЛК — импортировать необязательно».

Колонка «Статус матча» — 3 значения:

СтатусИконкаТекстПодсказка
new🆕Новый
matched_email🟡Совпадает emailTooltip: «У вас уже есть сотрудник {matched.first_name} {matched.last_name} с этим email. Выберите как поступить.»
already_linked🔗Уже импортированПод строкой серый текст: «Связан с {linked_employee.first_name} {linked_employee.last_name} ({linked_employee.email})»

Колонка «Действие» — выпадающий список, варианты зависят от статуса:

СтатусДоступные действия
newСоздать (default) / Пропустить
matched_emailСвязать (default) / Создать с другим email / Обновить / Пропустить
already_linked(selector disabled)

Кнопка «Далее →»

  • Активна если хотя бы один кандидат имеет действие отличное от Пропустить и Уже импортирован
  • Tooltip при disabled: «Выберите хотя бы одно действие отличное от «Пропустить»
  • Действие: переход на Шаг 2

Шаг 2: Дозаполнение полей

Для каждой строки с действием Создать, Создать с другим email, Обновить — раскрытая форма accordion. Для Связать и Пропустить — компактная свёрнутая строка без формы (нечего дозаполнять).

Раскрытая форма

▼ admin (новый, действие: Создать)

  Имя*           [Иван                ]   ← split(fio)[1] или пусто
  Фамилия*       [Петров              ]   ← split(fio)[0] или пусто
  Email*         [spad20@yandex.ru    ]   ← из PK
  
  Пароль*
    ◉ Сгенерировать (отправим reset-link на email)
    ○ Ввести вручную
        [_____________] [показать]
  
  Телефон        [+7  __________      ]
  PIN-код        [____]                  ← 4 цифры
  ☐ Курьер
  
  Роли и магазины:
    ☐ Бариста       Магазины: ☐ ТТ-1 ☐ ТТ-2 ☐ ТТ-3
    ☐ Менеджер зала Магазины: …
    ☐ Курьер        Магазины: …
    [+ Добавить роль]

Поля

ПолеКейс create_newКейс create_with_alt_emailКейс update_existing
first_namepre-fill из split(fio)[1], requiredpre-fill из split(fio)[1], requiredpre-fill из existing employee, disabled (но если в existing пусто — required)
last_namepre-fill из split(fio)[0], requiredpre-fill из split(fio)[0], requiredто же
emailpre-fill из PK, required, validate email formatпусто (PK email уже у другого), requireddisabled (от matched employee)
passwordradio Генерировать/Ввести (см. ниже)то жескрыто (не меняем существующий пароль)
generate_passwordtrue (default)то жеtrue (не используется)
phoneoptionaloptionalpre-fill, optional
pinoptional, 4 digitsто жеpre-fill, optional
is_courierdefault falseто жеpre-fill
roles[]пусто, optionalто жеpre-fill из existing, можно изменять

Split FIO

ФИО в PK — одна строка. Дефолт wizard’а: split по пробелу, первое слово → last_name, второе → first_name. Третье и далее (отчество) игнорируются. Если fio пустое или одно слово — соответствующие поля пусто, владелец вводит. Это деформируется при редких форматах PK — поэтому владелец всегда может скорректировать вручную.

Блок «Пароль»

Radio:

  • Сгенерировать (default) — backend генерирует случайный пароль, отправляет reset-link на email сотрудника. Hint под radio: «Сотруднику придёт письмо со ссылкой для установки пароля».
  • Ввести вручную — input password с кнопкой «глазик», валидация min 6 символов

Скрыт для update_existing — пароль при импорте через update не меняется.

Multi-select ролей и stores

По образцу карточки сотрудника (Сотрудники — Карточка вкладка «Роли и магазины»):

  • Загрузка ролей: GET /api/v1/admin/roles?status=active&hidden=false — только обычные (не скрытые)
  • Per-role чекбокс «Все ТТ» (default false) — иначе multi-select из ТТ scope’а

Скрытая роль владельца партнёра не доступна (фильтр hidden=false).

Валидация перед submit

  • Все required заполнены
  • Email формат
  • PIN — ровно 4 цифры (если введён)
  • Если выбраны роли — у каждой указан хотя бы один магазин ИЛИ галочка «Все ТТ»
  • Email уникален в рамках franchise_id (проверится backend’ом, см. §5 ошибки)

Кнопки внизу страницы

  • Назад (возврат на Шаг 1, decisions сохраняются в state)
  • Импортировать (submit) — disabled пока есть незаполненные required

Шаг 3: Результат

Прогресс

После клика «Импортировать» — спиннер «Импортирую {N} сотрудников…» с timeout 60s.

API: POST /api/v1/admin/paykeeper/accounts/{id}/employees/import

Если timeout (60s) — toast «Импорт занимает дольше обычного. Откройте «Журнал импортов» для проверки результата».

Экран успеха (status: success)

┌──────────────────────────────────────┐
│              ✓                       │
│      Импорт завершён                 │
│                                      │
│  Создано:    2 новых сотрудника      │
│  Связано:    1 существующий          │
│  Обновлено:  0                       │
│  Пропущено:  1                       │
│                                      │
│  [Перейти к сотрудникам]             │
│  [Импортировать ещё из этого ЛК]     │
│  [Импортировать из другого ЛК]       │
└──────────────────────────────────────┘

«Импортировать ещё из этого ЛК» — вернёт на Шаг 1 с тем же account_id (свежий fetch — добавятся новые 🔗 Уже импортирован для только что созданных).

«Импортировать из другого ЛК» — вернёт на /employees, открывая модалку выбора ЛК (если их >1) или сразу wizard для оставшегося.

Экран частичного успеха (status: partial)

┌──────────────────────────────────────┐
│              ⚠                       │
│      Импорт завершён частично        │
│                                      │
│  Создано:    1 новый сотрудник       │
│  Связано:    0                       │
│  Обновлено:  0                       │
│  Пропущено:  0                       │
│  Ошибок:     2                       │
│                                      │
│  Не удалось импортировать:           │
│  • et — Email уже используется       │
│           другим сотрудником         │
│  • koala — Не удалось создать        │
│            сотрудника (500)          │
│                                      │
│  [Перейти к сотрудникам]             │
│  [Подробнее об ошибках]              │
└──────────────────────────────────────┘

«Подробнее об ошибках» — модалка с полным errors_json (тот же UI что и в Журнале импортов §4).

Экран полного провала (status: failed)

Аналогично partial, но без счётчика «Создано». Текст: «Не удалось импортировать ни одного сотрудника».


4. Модалка «Журнал импортов»

Где открывается

Кнопка «Журнал импортов» на странице /employees рядом с «Выгрузить из PK». Видна при integrations.read AND есть хотя бы один импорт в истории (для любого ЛК во scope).

Если у пользователя несколько ЛК — модалка содержит фильтр по ЛК (default «Все ЛК»).

Загрузка

API: GET /api/v1/admin/paykeeper/accounts/{id}/employees/imports?limit=20

Если фильтр «Все ЛК» — backend возвращает union по всем активным во scope (либо фронт делает N запросов и склеивает; финальное решение — на этапе декомпозиции BFF).

Таблица

ВремяЛКЗапустилДлительностьСтатусСозданоСвязаноОбновленоПропущеноОшибок
27.04, 14:30exampleИван Петров8 сек⚠ Частично+2+1010
25.04, 10:15exampleИван Петров12 сек✓ Успешно+5+0000
20.04, 11:00petrovАнна Сидорова4 сек✓ Успешно+3+0000
  • Длительность = finished_at - started_at. Для running — «идёт N с».
  • Статус: ✓ Успешно (success), ⚠ Частично (partial), ✗ Не удалось (failed), ⟳ Идёт (running с polling 5s).

Раскрытие строки → детали

Клик на строку → inline-блок (accordion):

  • API GET /api/v1/admin/paykeeper/accounts/{id}/employees/imports/{run_id}
  • Если errors_json непустой — список:
    • admin (skipped) — пропущен пользователем
    • et (create_new) — Email уже используется другим сотрудником  
    • koala (create_new) — Не удалось создать сотрудника: 500 INTERNAL_ERROR
    
  • Если ошибок нет — серый текст «Без ошибок»

Пагинация

20 на страницу, кнопка «Загрузить ещё» внизу (использует since=lastSeenStartedAt).


5. Состояния и ошибки API

Код бэкаHTTPГде возникаетЧто показать
ACCOUNT_NOT_ACTIVE422Шаг 1 (preview), Шаг 3 (import)Блок «Интеграция PayKeeper приостановлена. Возобновите для импорта.» + «Закрыть»
PK_CONNECTION_FAILED422Шаг 1 (preview)Блок «Не удалось получить список из PayKeeper: {msg}» + «Повторить»
VALIDATION_ERROR400Шаг 3 (import)Подсветить проблемные строки на Шаге 2 (вернуться назад с error highlight)
CONFLICT (email уник.)409Шаг 3 (import)Пометить строку красным «Сотрудник с таким email уже существует» — пользователь возвращается в Шаг 2
FORBIDDEN403Любой шагToast «Недостаточно прав» (теоретически не должно случиться — кнопка disabled)
ACCOUNT_NOT_FOUND404Любой шагToast «PK-аккаунт не найден или удалён» + переход на /employees
NOT_FOUND (run)404Журнал импортов → деталиToast «Прогон не найден»

Особый кейс: PK вернул пустой список

Шаг 1 (preview) вернул data: []:

В этом ЛК PayKeeper нет пользователей.

Заведите кассиров в ЛК PayKeeper:
Настройки → Доступ к панели администратора

Затем повторите импорт.

[Закрыть]

6. Permissions — сводка

ДействиеPermission
Кнопка «Выгрузить из PK» виднаintegrations.read
Кнопка «Выгрузить из PK» активнаintegrations.manage AND employees.edit AND ≥1 active PK-аккаунт
Шаг 1 (preview)integrations.manage AND employees.edit
Шаги 2–3 (import)integrations.manage AND employees.edit
Кнопка «Журнал импортов» виднаintegrations.read
Просмотр прогона в журналеintegrations.read

integrations.read / integrations.manage / employees.editсуществующие permissions, новые не вводим (см. спека и Роли).


7. Ссылки