Admin Franchise web — BR 3.5
Контракты
- Фронт-спека wizard’а: Импорт сотрудников из PayKeeper
- API клиент: см. Admin BFF · API клиент в web
Что делаем
Кнопки на /employees
-
web/src/pages/employees/ListPage.tsx:- Добавить кнопки «Выгрузить из PK» и «Журнал импортов» в шапку рядом с «Добавить сотрудника»
- Видимость:
- «Выгрузить из PK» — если
hasPermission('integrations.read') - Активна —
hasPermission('integrations.manage')ANDhasPermission('employees.edit')AND есть хоть одинactivePK-аккаунт во scope (через queryusePaykeeperAccounts({ status: 'active' })) - Tooltip при disabled — соответствующая причина (см. фронт-спека §1)
- «Выгрузить из PK» — если
- Onclick:
- Если
accounts.length === 1→navigate('/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 paramaccount_id - Crumbs: «Сотрудники / Импорт из PayKeeper»
- Step indicator (3 шага)
- state-machine (XState или просто
useState<'preview' | 'fill' | 'submitting' | 'result'>):preview— Шаг 1fill— Шаг 2submitting— между Шагом 2 и 3 (спиннер)result— Шаг 3
- Route:
Шаг 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 пока есть невалидные)
- Список accordion-блоков: только для строк с действием
Шаг 3: Результат (<ResultStep />)
-
web/src/components/paykeeper/import/ResultStep.tsx:- Принимает
result: ImportRunSummary+errors: ImportError[] - Рендер по
status:success— зелёная галочка + счётчики + 2 CTApartial— жёлтый ⚠ + счётчики + блок «Не удалось импортировать» (топ-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 fieldsResultStepрендерит 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
- На
erp-test.nirbi.ruпод demo-coffee → /employees → видна кнопка «Выгрузить из PK» (active) - Клик → wizard загружается, показывает 4 кандидата из koala-test
- Выбрать действия → Далее → Шаг 2 показывает формы с pre-filled fio (split)
- Заполнить required, выбрать роль + ТТ → Импортировать → Шаг 3 показывает success
- /employees → новые сотрудники в списке
- Журнал импортов → модалка показывает прогон
- Повторный импорт → preview показывает уже импортированных как
already_linked