Admin Franchise web — BR 3.5

Контракты

Что делаем

Кнопки на /employees

  • web/src/pages/employees/ListPage.tsx:
    • Добавить кнопки «Выгрузить из PK» и «Журнал импортов» в шапку рядом с «Добавить сотрудника»
    • Видимость:
      • «Выгрузить из PK» — если hasPermission('integrations.read')
      • Активна — hasPermission('integrations.manage') AND hasPermission('employees.edit') AND есть хоть один active PK-аккаунт во scope (через query usePaykeeperAccounts({ status: 'active' }))
      • Tooltip при disabled — соответствующая причина (см. фронт-спека §1)
    • Onclick:
      • Если accounts.length === 1navigate('/employees/import-from-paykeeper?account_id=' + accounts[0].id)
      • Если accounts.length > 1 → открыть модалку <SelectPkAccountModal />

Модалка выбора ЛК

  • web/src/components/paykeeper/SelectPkAccountModal.tsx:
    • Принимает accounts: PkAccount[], onClose, onSelect(accountId)
    • Radio-list с именем ЮЛ + хост ЛК
    • Кнопка «Продолжить» — disabled пока ничего не выбрано
    • При submit → onSelect(accountId)navigate('/employees/import-from-paykeeper?account_id=' + selected)

Страница wizard’а

  • web/src/pages/employees/ImportFromPaykeeperPage.tsx:
    • Route: /employees/import-from-paykeeper, query param account_id
    • Crumbs: «Сотрудники / Импорт из PayKeeper»
    • Step indicator (3 шага)
    • state-machine (XState или просто useState<'preview' | 'fill' | 'submitting' | 'result'>):
      • preview — Шаг 1
      • fill — Шаг 2
      • submitting — между Шагом 2 и 3 (спиннер)
      • result — Шаг 3

Шаг 1: Список кандидатов (<PreviewStep />)

  • web/src/components/paykeeper/import/PreviewStep.tsx:
    • Загрузка через previewUserImport(accountId) (см. Admin BFF)
    • Состояния: loading (skeleton-таблица), error (PK_CONNECTION_FAILED / ACCOUNT_NOT_ACTIVE / network), empty list
    • Таблица с колонками: чекбокс, Логин PK, Email, ФИО, Admin, Статус матча, Действие
    • Per-row state: выбранное действие (хранится в parent state в decisions: ImportDecision[])
    • Бейдж «система» для pk_login IN ('admin', 'user')
    • Tooltip для matched_email — отображает существующего employee
    • Bulk-actions (опционально на P0): чекбоксы + кнопочный ряд «Применить ко всем выбранным: [действие ▼]»
    • Кнопка «Далее →» — disabled пока все decisions = skip или already_linked

Шаг 2: Дозаполнение (<FillStep />)

  • web/src/components/paykeeper/import/FillStep.tsx:
    • Список accordion-блоков: только для строк с действием create_new / create_with_alt_email / update_existing
    • Per-row форма с полями:
      • first_name (pre-fill из split(fio)[1] или existing employee)
      • last_name (pre-fill из split(fio)[0])
      • email (pre-fill / disabled / required в зависимости от action)
      • password block — radio Сгенерировать/Ввести (скрыт для update_existing)
      • phone (optional)
      • pin (optional, ровно 4 цифры)
      • is_courier (checkbox)
      • roles[] + per-role store_ids — переиспользуем существующий компонент <EmployeeRolesEditor /> из карточки сотрудника
    • Валидация (zod / yup) перед переходом на Шаг 3:
      • Все required заполнены
      • Email формат
      • PIN ровно 4 цифры если введён
      • У каждой выбранной роли — хотя бы один store ИЛИ галочка «Все ТТ»
    • Кнопки: «← Назад», «Импортировать» (disabled пока есть невалидные)

Шаг 3: Результат (<ResultStep />)

  • web/src/components/paykeeper/import/ResultStep.tsx:
    • Принимает result: ImportRunSummary + errors: ImportError[]
    • Рендер по status:
      • success — зелёная галочка + счётчики + 2 CTA
      • partial — жёлтый ⚠ + счётчики + блок «Не удалось импортировать» (топ-3 ошибки) + кнопка «Подробнее об ошибках»
      • failed — красный ✗ + блок ошибок
    • CTA-кнопки:
      • «Перейти к сотрудникам» → navigate('/employees')
      • «Импортировать ещё из этого ЛК» → reset state, fetch preview снова
      • «Импортировать из другого ЛК» → navigate('/employees') + автоматически открыть <SelectPkAccountModal />
    • Модалка «Подробнее об ошибках» — список из errors_json (формат: pk_login (action) — message)

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

  • web/src/components/paykeeper/UserImportsLogModal.tsx:
    • Открывается с /employees по кнопке «Журнал импортов»
    • Если у пользователя несколько ЛК — добавить фильтр select «ЛК» вверху модалки
    • Загрузка через listUserImports(accountId, { limit: 20 })
    • Таблица: Время, ЛК (если фильтр «Все»), Запустил, Длительность, Статус, счётчики
    • Раскрытие строки — accordion с подгрузкой getUserImportDetails(accountId, runId)
    • Пагинация: «Загрузить ещё» с since=lastSeenStartedAt

Permissions hook

  • Расширить usePermissions — для удобства добавить вспомогательные хуки:
    • useCanImportPkUsers()boolean — комбо integrations.manage + employees.edit + есть active PK-аккаунт

Routing

  • web/src/App.tsx или роутер:
    • Добавить <Route path="/employees/import-from-paykeeper" element={<ImportFromPaykeeperPage />} />
    • Гард: redirect на /employees если нет permissions

Тесты

  • React Testing Library:
    • PreviewStep рендерит 3 разных match_status корректно
    • FillStep валидирует required fields
    • ResultStep рендерит partial/failed с ошибками
    • SelectPkAccountModal блокирует «Продолжить» пока не выбран аккаунт
  • e2e (Playwright или подобное) — happy-path импорта 1 сотрудника

Не делаем

  • ❌ Bulk-actions в Шаге 1 (опционально на P1) — но оставить чекбоксы в DOM для будущего расширения
  • ❌ Отдельный route /paykeeper-users — wizard вызывается только из /employees
  • ❌ State persistence (если refresh страницы посреди wizard’а) — пользователь начинает заново
  • ❌ Drag&drop / advanced UX — minimum viable wizard

Verification

  1. На erp-test.nirbi.ru под demo-coffee → /employees → видна кнопка «Выгрузить из PK» (active)
  2. Клик → wizard загружается, показывает 4 кандидата из koala-test
  3. Выбрать действия → Далее → Шаг 2 показывает формы с pre-filled fio (split)
  4. Заполнить required, выбрать роль + ТТ → Импортировать → Шаг 3 показывает success
  5. /employees → новые сотрудники в списке
  6. Журнал импортов → модалка показывает прогон
  7. Повторный импорт → preview показывает уже импортированных как already_linked

Ссылки