feat(s29): SuperAdmin UI — бейдж + переключатель расширенного режима (шаг 6/8)

- SuperAdminOrganizationsPage:
  - OrgRow.isAdvancedMode добавлен.
  - Новая колонка «Режим» — бейдж «Простой» (slate) / «Расширенный»
    (violet).
  - Кнопка-экшн (иконка Sparkles) открывает модалку подтверждения
    с разной копией для вкл/выкл; успешный PUT → list.refetch().
  - Выкл-копия напоминает что данные НЕ удаляются и переключение
    обратимо; вкл-копия перечисляет открывающиеся модули.

Переключение из карточки организации — отложено, список-страница
с бейджем и кнопкой закрывает UX.

pnpm run build — ✓ 2.02s, 0 TS errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-04 11:50:18 +05:00
parent 792ea0fd41
commit 276cba22f8
2 changed files with 80 additions and 2 deletions

View file

@ -14,7 +14,7 @@ SuperAdmin и только для конкретной организации.
- [x] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях
- [x] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
- [x] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
- [ ] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
- [x] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
- [ ] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
- [ ] 8. Деплой стейджа + `docs/simple-mode.md`
@ -131,3 +131,15 @@ SuperAdmin и только для конкретной организации.
Что отложено (новая функциональность, в этой задаче не трогаем):
- `Counterparty.IsSupplier` / `IsBuyer` — задача просит галочки «Поставщик/Покупатель», но на `Counterparty` таких bool-колонок нет. Это новая миграция и расширение API — отдельная задача.
- `pnpm run build` — ✓ (2.36s, 0 TS errors).
### 2026-10-04 — Шаг 6 ✓
- `SuperAdminOrganizationsPage.tsx`:
- В `OrgRow` добавлен `isAdvancedMode: boolean` (бэк уже отдаёт из шага 2).
- Новая колонка **«Режим»** с бейджем «Простой» (slate-пилюля) / «Расширенный» (violet-пилюля).
- Новая кнопка-экшн рядом с «Архивировать» (иконка Sparkles): при клике открывается модалка подтверждения с разной копией для включения/выключения:
- *Включить:* перечисляет, что станет доступно (demand, loyalty, promos, abc, audit, несколько цен, пресеты, bulk-ops, telegram, sso, язык).
- *Выключить:* предупреждает что данные НЕ удаляются и переключение можно вернуть; напоминает проверить активные операции.
- Переключение бьёт `PUT /api/super-admin/organizations/{id}/advanced-mode { enabled }` и после успеха `list.refetch()`.
- Переключение из карточки организации (`SuperAdminOrgEmployeesPage` / detail-view) не делаем в этой итерации — список-страница с бейджем и кнопкой уже закрывает UX.
- `pnpm run build` — ✓ (2.02s, 0 TS errors).

View file

@ -1,7 +1,7 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { Plus, Archive, RotateCcw, Trash2, LogIn, Users } from 'lucide-react'
import { Plus, Archive, RotateCcw, Trash2, LogIn, Users, Sparkles } from 'lucide-react'
import { api, setOrgOverride } from '@/lib/api'
import { ListPageShell } from '@/components/ListPageShell'
import { DataTable } from '@/components/DataTable'
@ -18,6 +18,7 @@ interface OrgRow {
id: string; name: string; countryCode: string
isActive: boolean; isArchived: boolean; archivedAt: string | null
createdAt: string; employeeCount: number; productCount: number; lastLoginAt: string | null
isAdvancedMode: boolean
}
export function SuperAdminOrganizationsPage() {
@ -27,6 +28,22 @@ export function SuperAdminOrganizationsPage() {
const [archiveOf, setArchiveOf] = useState<OrgRow | null>(null)
const [deleteOf, setDeleteOf] = useState<OrgRow | null>(null)
const [confirmName, setConfirmName] = useState('')
// Sprint 29 — модалка подтверждения переключения режима; `toggleMode.target`
// хранит и строку, и целевое состояние (вкл./выкл.).
const [toggleModeOf, setToggleModeOf] = useState<{ row: OrgRow; enable: boolean } | null>(null)
const [togglePending, setTogglePending] = useState(false)
const applyToggleMode = async () => {
if (!toggleModeOf) return
setTogglePending(true)
try {
await api.put(`${URL}/${toggleModeOf.row.id}/advanced-mode`, { enabled: toggleModeOf.enable })
setToggleModeOf(null)
list.refetch()
} finally {
setTogglePending(false)
}
}
const archive = async () => {
if (!archiveOf || confirmName !== archiveOf.name) return
@ -107,6 +124,9 @@ export function SuperAdminOrganizationsPage() {
{ header: 'Статус', width: '120px', cell: (r) => r.isArchived
? <span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">Архив</span>
: <span className="text-xs text-emerald-600">Активна</span> },
{ header: 'Режим', width: '130px', cell: (r) => r.isAdvancedMode
? <span className="text-xs px-1.5 py-0.5 rounded bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200">Расширенный</span>
: <span className="text-xs px-1.5 py-0.5 rounded bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300">Простой</span> },
{ header: '', width: '160px', cell: (r) => (
<div className="flex gap-1.5" onClick={(e) => e.stopPropagation()}>
{!r.isArchived && (
@ -121,6 +141,16 @@ export function SuperAdminOrganizationsPage() {
className="p-1.5 text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded">
<Users className="w-4 h-4" />
</button>
{!r.isArchived && (
<button
title={r.isAdvancedMode ? 'Выключить расширенный режим' : 'Включить расширенный режим'}
onClick={() => setToggleModeOf({ row: r, enable: !r.isAdvancedMode })}
className={`p-1.5 rounded ${r.isAdvancedMode
? 'text-violet-600 hover:bg-violet-50 dark:hover:bg-violet-900/20'
: 'text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800'}`}>
<Sparkles className="w-4 h-4" />
</button>
)}
{!r.isArchived ? (
<button title="Архивировать" onClick={() => { setArchiveOf(r); setConfirmName('') }}
className="p-1.5 text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded">
@ -167,6 +197,42 @@ export function SuperAdminOrganizationsPage() {
)}
</Modal>
<Modal
open={!!toggleModeOf}
onClose={() => setToggleModeOf(null)}
title={toggleModeOf?.enable ? 'Включить расширенный режим' : 'Выключить расширенный режим'}
footer={<>
<Button variant="secondary" onClick={() => setToggleModeOf(null)} disabled={togglePending}>Отмена</Button>
<Button onClick={applyToggleMode} disabled={togglePending}>
{togglePending ? 'Применяю…' : (toggleModeOf?.enable ? 'Включить' : 'Выключить')}
</Button>
</>}
>
{toggleModeOf && (
<div className="space-y-3 text-sm">
{toggleModeOf.enable ? (
<>
<p>У организации <strong>{toggleModeOf.row.name}</strong> будет доступен полный набор функций:
оптовые отгрузки, перемещения между точками, лояльность, промо/акции,
ABC-анализ, аудит организации, несколько типов цен, bulk-операции и
сохранённые пресеты, Telegram-бот владельца, SSO, выбор языка.</p>
<p className="text-slate-500">Переключение логируется в SuperAdmin audit-log.</p>
</>
) : (
<>
<p>У организации <strong>{toggleModeOf.row.name}</strong> останется только базовый
розничный учёт. Советуем сначала убедиться, что сотрудники не ведут активных
операций в скрываемых модулях.</p>
<p className="text-slate-500">
Данные скрытых модулей <strong>не удаляются</strong> — при повторном включении
всё снова будет видно. Переключение логируется в SuperAdmin audit-log.
</p>
</>
)}
</div>
)}
</Modal>
<Modal open={!!deleteOf} onClose={() => setDeleteOf(null)}
title="Удалить организацию навсегда"
footer={<>