Admin Franchise — BR 1.4.4

Репо: erp-admin

Зависимости

Бэкенд (User, Auth) должен быть собран и задеплоен под новые контракты BR 1.4.4.


BFF (bff/src/)

Новые proxy routes

  • В routes/legal-entities.ts добавить:
    • GET /api/v1/admin/legal-entities/:id/owner-permissions → User Service
    • PUT /api/v1/admin/legal-entities/:id/owner-permissions → User Service
  • Новый routes/franchises.ts:
    • GET /api/v1/admin/franchises/:id → User Service /api/v1/franchises/{id}
  • Регистрация в server.ts

Обновление existing

  • routes/auth.tsGET /auth/me response прозрачно проксируется (структура меняется у Auth Service, BFF не правим логику)
  • routes/employees.ts — payload create/update: убрать поле role из shape (если оно где-то жёстко валидируется в BFF-обёртке)

Web (web/src/)

Навигация и gating

  • Layout.tsx / Sidebar.tsx — пункт «Юр. лица» скрывается при franchise.type === 'individual'
  • App.tsx / роутер — /admin/legal-entities/* редиректит на /admin/dashboard при individual, либо показывает 404-страницу

PermissionContext расширение

  • contexts/PermissionContext.tsx:
    • State: добавить franchise: { id, type } и scope: { type, legal_entity_ids?, store_ids? } — из /auth/me response
    • Hooks: useFranchiseType(): 'corporate' | 'individual', useScope(): Scope, useIsOwner(): boolean
  • Новый компонент <HideForIndividual>{children}</HideForIndividual> — гейтинг по типу франшизы

API client

  • api/ownerPermissions.ts:
    • getOwnerPermissions(legalEntityId)
    • updateOwnerPermissions(legalEntityId, payload)
  • api/franchises.ts:
    • getFranchise(id) — если где-то нужен отдельный вызов помимо /auth/me

Страницы сотрудников

  • pages/employees/CreatePage.tsx:
    • Убрать Select «Multi-tenancy роль» (Менеджер/Кассир)
    • Убрать Select «Торговая точка» (одиночный)
    • Оставить секцию «Роли» (repeater из BR 1.4.3 — уже реализована)
    • Payload: убрать role, store_ids (верхний уровень); оставить только roles[]
  • pages/employees/EditPage.tsx — аналогично CreatePage
  • pages/employees/ListPage.tsx:
    • Убрать колонку «Multi-tenancy роль»
    • Убрать фильтр «Multi-tenancy роль»
    • Колонка «Торговые точки» — рассчитывается как уникальный набор из roles[].store_ids
    • В колонке ФИО: добавить бейдж «Владелец франшизы» / «Владелец партнёра» при соответствующем scope (получать scope для каждого сотрудника не будем — только для текущего user из /auth/me; для остальных — бэк вернёт флаг is_owner или рассчитаем через legal_entities.owner_user_id. Возможно потребует доработки API)
  • pages/employees/ViewPage.tsx — убрать отображение enum-роли, оставить блок permissions-ролей

Карточка ЮЛ партнёра

  • pages/legal-entities/ViewPage.tsx:
    • Переделать на табы: Реквизиты | Владелец | Права | ТТ (вкладка «Права» только для type=franchisee)
    • Компонент tabs/OwnerPermissionsTab.tsx — новый:
      • Radio «Полный доступ» / «Настроенные права»
      • При «Настроенные» — матрица permissions (переиспользовать BackofficePermissionsTab + PosPermissionsTab из Roles)
      • Минимум (pos.access, stores.read, employees.read) — checkbox’ы disabled
      • Кнопка «Сохранить» → updateOwnerPermissions(le_id, payload)

Форма создания ЮЛ партнёра

  • pages/legal-entities/CreatePage.tsx:
    • Добавить секцию «Права владельца» (reuse компонент вкладки Права)
    • Payload расширить owner_permissions: { mode, permissions? }

Shared types (shared/src/types/)

  • employee.ts:
    • Убрать role: EmployeeRole из Employee, из CreateEmployeeRequest, из UpdateEmployeeRequest
    • Убрать store_ids как отдельное поле (осталось только roles[].store_ids)
    • Убрать тип EmployeeRole совсем (если не используется в старом коде)
  • role.ts:
    • Добавить ownerLegalEntityId?: string в Role
    • Pagination response для /roles уже фильтрует скрытые — без правок клиента
  • auth.ts (или равнозначный):
    • MeResponse: убрать role, store_ids, legal_entity_id
    • Добавить franchise: { id, type }, scope: { type, legal_entity_ids?, store_ids? }
  • Новый franchise.ts:
    • Franchise { id, name, type: 'corporate'|'individual', created_at }
    • FranchiseType = 'corporate' | 'individual'
    • Scope { type: 'all_franchise'|'legal_entity_ids'|'store_ids', legal_entity_ids?, store_ids? }
  • legal-entity.ts:
    • OwnerPermissions { mode: 'full'|'custom', permissions: string[] }
    • CreateLegalEntityRequest — добавить owner_permissions?: OwnerPermissions

Списки страниц — фильтры по ТТ

В ListPage сотрудников / складских операций / заказов — набор доступных ТТ для фильтра должен браться из:

  • scope.type=all_franchise → все ТТ франшизы (GET /stores)
  • scope.type=legal_entity_ids → ТТ из этих ЮЛ
  • scope.type=store_ids → только эти ТТ

Пока серверная фильтрация — можно не менять UI: API сам отдаёт только доступные данные. Но в dropdown ТТ — фильтр по scope имеет смысл.

Тестирование (manual)

  • Login под admin@erp.local — видит «Юр. лица» (type=corporate)
  • (Симуляция) — если franchise.type=individual — раздел «Юр. лица» скрыт, URL /legal-entities редиректит
  • Создать партнёра «Петров» с режимом «Полный доступ» → у Петрова системная «Администратор»
  • Создать партнёра «Сидоров» с режимом «Настроенные права» с ограничением (например, без stores.edit) → у Сидорова скрытая роль
  • В карточке ЮЛ Петрова → вкладка «Права» → переключить режим → работает
  • В /admin/roles — ни Петрова, ни Сидорова персональных ролей не видно (только системная и обычные)
  • Форма создания сотрудника — только единый блок «Роли», никаких Менеджер/Кассир

Выходные критерии

  • TypeScript компиляция: 0 ошибок (web + shared)
  • BFF собирается (известные pre-existing ошибки не считаются)
  • Smoke-test full login → view legal entities → view owner permissions
  • Коммит + push