From 792ea0fd41cb7345fa7a57f0807ec23a896a2fb0 Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 4 Oct 2026 11:48:23 +0500 Subject: [PATCH] =?UTF-8?q?feat(s29):=20=D0=BF=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=20=E2=80=94=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80?= =?UTF-8?q?=D0=B0/=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=B3=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0/=D0=BF=D1=80=D0=B8=D1=91=D0=BC=D0=BA=D0=B8=20(?= =?UTF-8?q?=D1=88=D0=B0=D0=B3=205/8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Везде формула: simpleMode = me && !me.advancedMode && !SuperAdmin. Значения скрытых полей в state сохраняются и при save на сервер отправляются как есть; данные НЕ стираются. - ProductEditPage: скрыты Артикул (auto-gen), Описание, Страна, Эталонная цена, Ставка НДС, «В т.ч. НДС», Валюта закупки, «Услуга», «Маркируемый», Максимальный остаток, не-retail типы цен в блоке «Цены продажи». - CounterpartiesPage: скрыты Юр.название, Страна, Email, Банк, Р.счёт, БИК, Контактное лицо. Остаются: Название, Тип лица, БИН/ИИН, Адрес, Телефон, Заметки. - SupplyEditPage: колонка «Валюта» всегда скрыта в простом режиме (одна валюта по умолчанию). «Поставщик/Покупатель»-галочки отложены — требуют новых колонок IsSupplier/IsBuyer на Counterparty (новая функциональность). pnpm run build — ✓ 2.36s, 0 TS errors. Co-Authored-By: Claude Opus 4.7 --- docs/simple-mode-progress.md | 25 ++++++- .../src/pages/CounterpartiesPage.tsx | 68 ++++++++++++------- .../src/pages/ProductEditPage.tsx | 59 ++++++++++++---- .../src/pages/SupplyEditPage.tsx | 8 ++- 4 files changed, 118 insertions(+), 42 deletions(-) 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 })} /> + + + )}