diff --git a/docs/simple-mode-progress.md b/docs/simple-mode-progress.md index b5f2bb4..00b6c40 100644 --- a/docs/simple-mode-progress.md +++ b/docs/simple-mode-progress.md @@ -13,7 +13,7 @@ SuperAdmin и только для конкретной организации. - [x] 2. API `PUT /api/super-admin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403 - [x] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях - [x] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно» -- [ ] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются +- [x] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются - [ ] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением - [ ] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости - [ ] 8. Деплой стейджа + `docs/simple-mode.md` @@ -108,3 +108,26 @@ SuperAdmin и только для конкретной организации. - `DashboardPage` — в простом режиме скрыты виджеты «Топ товаров» и «Маржа». Остаются KPI (выручка today/week/month, средний чек), график продаж, «Последние продажи», «Низкий остаток» — ровно то, что в чек-листе. - Данные НЕ удаляются при выключении режима: это только UI-гейтинг + серверный 403; при повторном включении всё видно. - `pnpm run build` — ✓ (2.60s, 0 TS errors). + +### 2026-10-04 — Шаг 5 ✓ + +- Везде формула простого режима: `simpleMode = me && !me.advancedMode && !roles.includes('SuperAdmin')`. SuperAdmin не ощущает — ему доступно всё. +- **Карточка товара** (`ProductEditPage.tsx`) в простом режиме прячет (значения в state сохраняются, при save не стираются): + - «Артикул» (генерируется автоматически, Название на 3 колонки) + - «Описание», «Страна происхождения», «Эталонная цена» + - «Ставка НДС», чекбокс «В том числе НДС» + - «Валюта закупки» (мульти-валюта) + - Чекбоксы «Услуга», «Маркируемый» + - «Максимальный остаток» — остаётся только «Минимальный» + - Все не-retail типы цен в блоке «Цены продажи» — рендерится только retail/system price type. +- **Контрагенты** (`CounterpartiesPage.tsx`) — в простом режиме скрыты Юр. название, Страна, Email, Банк/Р.счёт/БИК, Контактное лицо. Остаются: Название, Тип лица, БИН/ИИН, Адрес, Телефон, Заметки. +- **Приёмка** (`SupplyEditPage.tsx`) — колонка «Валюта» всегда скрыта в простом режиме (одна валюта по умолчанию). +- Другие документы (Enter, Loss, Inventory, SupplierReturn) используют те же `org.ShowXxx` / `org.MultiCurrencyEnabled` флаги — уже скрывают advanced-поля для магазинов с «простым» org-настройками. + +Доп. список спрятанного за `simpleMode` (из шагов 4/5, не вошёл напрямую в чек-лист): +- `AppLayout` → `LanguageSwitcher` +- `DashboardPage` → виджеты «Топ товаров» и «Маржа» + +Что отложено (новая функциональность, в этой задаче не трогаем): +- `Counterparty.IsSupplier` / `IsBuyer` — задача просит галочки «Поставщик/Покупатель», но на `Counterparty` таких bool-колонок нет. Это новая миграция и расширение API — отдельная задача. +- `pnpm run build` — ✓ (2.36s, 0 TS errors). diff --git a/src/food-market.web/src/pages/CounterpartiesPage.tsx b/src/food-market.web/src/pages/CounterpartiesPage.tsx index 2caed52..2a6998e 100644 --- a/src/food-market.web/src/pages/CounterpartiesPage.tsx +++ b/src/food-market.web/src/pages/CounterpartiesPage.tsx @@ -17,6 +17,7 @@ import { PhoneInput } from '@/components/PhoneInput' import { ConfirmDialog } from '@/components/ConfirmDialog' import { useConfirm } from '@/lib/useConfirm' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' +import { useMe } from '@/lib/useMe' import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types' const URL = '/api/catalog/counterparties' @@ -56,6 +57,11 @@ const typeLabel: Record = { export function CounterpartiesPage() { const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList(URL) const { create, update, remove } = useCatalogMutations(URL, URL) + const me = useMe() + // Sprint 29 — простой режим скрывает редко нужные поля (юр.название, + // банк, страна, контактное лицо, email). Значения существующих + // контрагентов сохраняются и при переключении в расширенный снова видны. + const simpleMode = !!me.data && !me.data.advancedMode && !me.data.roles?.includes('SuperAdmin') const [form, setForm] = useState
(null) const [fieldErrors, setFieldErrors] = useState>>({}) const { confirm, dialogProps } = useConfirm() @@ -165,9 +171,11 @@ export function CounterpartiesPage() { onChange={(e) => { setForm({ ...form, name: e.target.value }); if (fieldErrors.name) setFieldErrors(p => ({...p, name: undefined})) }} onBlur={() => { if (!form.name.trim()) setFieldErrors(p => ({...p, name: 'Название обязательно'})) }} /> - - setForm({ ...form, legalName: e.target.value })} /> - + {!simpleMode && ( + + setForm({ ...form, legalName: e.target.value })} /> + + )} setForm({ ...form, countryId: e.target.value })}> - - {countries.data?.map((c) => )} - - + {!simpleMode && ( + + + + )} setForm({ ...form, address: e.target.value })} /> @@ -194,23 +204,29 @@ export function CounterpartiesPage() { onChange={(v) => { setForm({ ...form, phone: v }); if (fieldErrors.phone) setFieldErrors(p => ({...p, phone: undefined})) }} onBlur={() => { if (form.phone) { const e = validatePhone(form.phone); if (e) setFieldErrors(p => ({...p, phone: e})) } }} /> - - { setForm({ ...form, email: e.target.value }); if (fieldErrors.email) setFieldErrors(p => ({...p, email: undefined})) }} - onBlur={() => { if (form.email) { const e = validateEmail(form.email); if (e) setFieldErrors(p => ({...p, email: e})) } }} /> - - - setForm({ ...form, bankName: e.target.value })} /> - - - setForm({ ...form, bankAccount: e.target.value })} /> - - - setForm({ ...form, bik: e.target.value })} /> - - - setForm({ ...form, contactPerson: e.target.value })} /> - + {!simpleMode && ( + + { setForm({ ...form, email: e.target.value }); if (fieldErrors.email) setFieldErrors(p => ({...p, email: undefined})) }} + onBlur={() => { if (form.email) { const e = validateEmail(form.email); if (e) setFieldErrors(p => ({...p, email: e})) } }} /> + + )} + {!simpleMode && ( + <> + + setForm({ ...form, bankName: e.target.value })} /> + + + setForm({ ...form, bankAccount: e.target.value })} /> + + + setForm({ ...form, bik: e.target.value })} /> + + + setForm({ ...form, contactPerson: e.target.value })} /> + + + )}