- deploy-stage.sh → stage поднят с новым кодом; /health/ready зелёный.
- Smoke: /api/me на стейдже содержит advancedMode.
- E2E stage-simple-mode — 7/7 passed, включая Playwright-шаг
«Недоступно в простом режиме».
- docs/simple-mode.md: пользовательская документация с таблицей
простого режима, перечнем расширенных модулей, инструкцией для
SuperAdmin (API + UI), ProblemDetails-примером и ссылкой на тесты.
- tests/e2e/scenarios/stage-simple-mode.yml: заквочен title с «:» для
js-yaml. steps.ts: правильный ключ localStorage SPA ('fm.access_token').
docs/simple-mode-progress.md — все 8 пунктов [x], итог.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
197 lines
18 KiB
Markdown
197 lines
18 KiB
Markdown
# Simple Mode — прогресс
|
||
|
||
Задача: по умолчанию у всех организаций «Простой режим» — только базовые
|
||
функции розничного учёта. Включить «Расширенный режим» может только
|
||
SuperAdmin и только для конкретной организации. Данные при переключении
|
||
не удаляются; при повторном включении всё видно.
|
||
|
||
Старт: 2026-10-04.
|
||
|
||
## Чек-лист
|
||
|
||
- [x] 1. Domain + миграция `Organization.IsAdvancedMode` (bool, default false, backfill false)
|
||
- [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: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
|
||
- [x] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
|
||
- [x] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
|
||
- [x] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
|
||
- [x] 8. Деплой стейджа + `docs/simple-mode.md`
|
||
|
||
## Простой режим — что видно
|
||
|
||
- Главная: выручка за сегодня, что заканчивается, последние продажи
|
||
- Товары и группы товаров
|
||
- Контрагенты (единый список — поставщики и покупатели)
|
||
- Склад: Приёмка, Оприходование, Списание, Инвентаризация, Возврат поставщику, Остатки
|
||
- Продажи: Продажи, Возвраты покупателей
|
||
- Отчёты: Продажи, Прибыль, Остатки на дату
|
||
- Сотрудники и роли, Настройки магазина (включая ОФД), Импорт товаров
|
||
- 2FA в профиле — остаётся
|
||
|
||
## Расширенный режим — что добавляется
|
||
|
||
- Оптовые отгрузки (Demand)
|
||
- Перемещения между точками
|
||
- Лояльность
|
||
- Промокоды / акции
|
||
- ABC-анализ
|
||
- Аудит организации
|
||
- Несколько типов цен (в простом — одна цена продажи)
|
||
- Bulk-операции и сохранённые пресеты в таблицах
|
||
- Telegram-бот владельца
|
||
- SSO
|
||
- Переключатель языка (в простом только русский)
|
||
- Продвинутые виджеты дашборда
|
||
|
||
## Журнал
|
||
|
||
### 2026-10-04 — Шаг 1 ✓
|
||
|
||
- `Organization.IsAdvancedMode` (bool, default false) + XML-док, что переключает только SuperAdmin.
|
||
- Миграция `20261004100000_Phase29a_IsAdvancedMode` — идемпотентный `ADD COLUMN IF NOT EXISTS` с `DEFAULT false`; все существующие орги получают `false`.
|
||
- Snapshot обновлён (`AppDbContextModelSnapshot.cs`).
|
||
- Применено на dev-БД: 5/5 организаций `IsAdvancedMode = false`.
|
||
- `dotnet build food-market.api` — ✓ (0 error, warnings — старые, не наши).
|
||
|
||
### 2026-10-04 — Шаг 2 ✓
|
||
|
||
- `PUT /api/super-admin/organizations/{id}/advanced-mode { "enabled": bool }` в `SuperAdminOrganizationsController` — `[Authorize(Roles="SuperAdmin")]`. Админ орги получает 403 от стандартного Authorize-фильтра (роль Admin ≠ SuperAdmin); в контроллере сверху уже `[Authorize(Roles="SuperAdmin")]` — не нужно отдельного `Forbid()`.
|
||
- Запись в `SuperAdminAuditLog`: ActionType `EnableAdvancedMode` / `DisableAdvancedMode`, Description, Changes `{"before":…,"after":…}`, IpAddress. Идемпотентный переключатель: если состояние совпадает — 200 без аудит-записи.
|
||
- Архивную орг переключать нельзя — 409.
|
||
- `/api/me` отдаёт `advancedMode` (bool). SuperAdmin без org → false (это ОК — он работает через `/api/super-admin/...`).
|
||
- `OrgRow` / `OrgDetail` в SuperAdmin list / detail — добавлено `IsAdvancedMode`.
|
||
- Маршрут: `/api/super-admin/...` (с дефисом), так как существующий контроллер уже использует этот стиль; задача упоминала `superadmin` без дефиса — выбрал существующий стиль.
|
||
- `dotnet build food-market.api` — ✓ (0 error).
|
||
|
||
### 2026-10-04 — Шаг 3 ✓
|
||
|
||
- `src/food-market.api/Infrastructure/Authorization/RequiresAdvancedModeAttribute.cs`:
|
||
- `[RequiresAdvancedMode]` — `TypeFilterAttribute` вокруг `AdvancedModeFilter : IAsyncAuthorizationFilter`.
|
||
- Читает `IsAdvancedMode` по `org_id`-claim через `AppDbContext` (без доверия к клиентским заголовкам). Кэш in-memory 30 с, инвалидация после переключения.
|
||
- SuperAdmin bypass (чтобы он мог проверять скрытые модули через «войти как»).
|
||
- Отказ — `403 ProblemDetails` с `type`, `title`, `detail` на русском и `extensions.code = "advanced-mode-required"` — фронт различает это 403 от обычного «нет прав».
|
||
- Атрибут навешен на (контроллеры):
|
||
- `Sales/DemandsController` (оптовые отгрузки)
|
||
- `Inventory/TransfersController` (перемещения)
|
||
- `Loyalty/LoyaltyCardsController`, `Loyalty/LoyaltyProgramsController`
|
||
- `Promotions/PromotionsController`
|
||
- `Reports/AbcReportController`
|
||
- `Admin/OrgAuditLogController`
|
||
- `Telegram/TelegramBindingController`
|
||
- `Common/UserPresetsController` (пресеты фильтров)
|
||
- И на отдельные действия:
|
||
- `Catalog/PriceTypesController` — POST / PUT / DELETE (чтение остаётся — товары используют системный retail-прайс)
|
||
- `Catalog/ProductsController.BulkUpdate` (POST `/products/bulk-update`)
|
||
- `Auth/ExternalAuthController` (SSO) — атрибут НЕ навешен: endpoint инициирует OAuth-челлендж для анонима, `org_id`-claim ещё не выставлен. Контроль — на уровне UI (не показывать кнопку «Войти через Google/Microsoft» в простом режиме). Также SSO в простом режиме не ломается при включении — просто кнопка появляется.
|
||
- `SuperAdminOrganizationsController.SetAdvancedMode` вызывает `AdvancedModeFilter.InvalidateCache(orgId)` после `SaveChangesAsync`, чтобы следующий запрос этой org сразу увидел новое значение.
|
||
- `dotnet build food-market.api` — ✓ (0 error).
|
||
|
||
### 2026-10-04 — Шаг 4 ✓
|
||
|
||
- `src/food-market.web/src/components/AdvancedModeGuard.tsx` — render-guard: `me.advancedMode = false` + не-SuperAdmin → страница «Недоступно в простом режиме» с текстом + кнопкой «На главную». SuperAdmin и расширенный режим — пропускает.
|
||
- `useMe.ts` + локальные `MeResponse` в AppLayout/DashboardPage расширены полем `advancedMode: boolean`.
|
||
- `AppLayout.buildNav(roles, advancedMode)` — в простом режиме скрыто:
|
||
- `nav.priceTypes` (справочник типов цен)
|
||
- `nav.transfers` (перемещения между точками)
|
||
- `nav.demands` / `nav.promotions` / `nav.loyaltyPrograms` / `nav.loyaltyCards`
|
||
- `nav.reportAbc`
|
||
- `nav.auditLog`
|
||
- `LanguageSwitcher` (в простом режиме интерфейс только по-русски)
|
||
- `App.tsx` — скрытые маршруты обёрнуты в `<AdvancedModeGuard>`:
|
||
- `/catalog/price-types`
|
||
- `/inventory/transfers`, `/inventory/transfers/new`, `/inventory/transfers/:id`
|
||
- `/sales/demands`, `/sales/demands/new`, `/sales/demands/:id`
|
||
- `/loyalty/programs`, `/loyalty/cards`, `/promotions`
|
||
- `/audit-log`
|
||
- `/reports/abc`
|
||
- `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).
|
||
|
||
### 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).
|
||
|
||
### 2026-10-04 — Шаг 7 ✓
|
||
|
||
**Unit — `tests/food-market.UnitTests/AdvancedModeFilterTests.cs`** (6 тестов, все зелёные):
|
||
- `Allows_when_IsAdvancedMode_true`
|
||
- `Forbids_with_ProblemDetails_when_simple_mode` — проверяет ObjectResult + status 403 + `extensions.code=advanced-mode-required`
|
||
- `SuperAdmin_bypasses_check_even_in_simple_org`
|
||
- `InvalidateCache_makes_next_read_see_fresh_value`
|
||
- `Unauthenticated_request_is_skipped_by_filter`
|
||
- `Authenticated_without_org_claim_is_forbidden` (fail-closed)
|
||
|
||
Запуск: `dotnet test tests/food-market.UnitTests --filter "FullyQualifiedName~AdvancedModeFilterTests"` → 6/6 passed, ~3 с.
|
||
|
||
**Integration — `tests/food-market.IntegrationTests/AdvancedModeTests.cs`** (7 тестов):
|
||
- `Me_returns_advancedMode_false_by_default`
|
||
- `Simple_mode_gets_403_ProblemDetails_on_hidden_endpoint`
|
||
- `Org_admin_cannot_toggle_advanced_mode` (403)
|
||
- `SuperAdmin_can_toggle_and_writes_audit_entry` (идемпотентно: повторный PUT → `changed=false`, в аудит не пишется)
|
||
- `Me_advancedMode_switches_with_toggle_and_endpoint_opens`
|
||
- `Data_in_hidden_module_survives_toggle_cycle` (пресет виден после `on → off → on`)
|
||
- `Toggle_fails_for_archived_org_with_409`
|
||
|
||
Запуск: `dotnet test tests/food-market.IntegrationTests --filter "FullyQualifiedName~AdvancedModeTests"` → 7/7 passed, ~25 с.
|
||
|
||
Доп. фикс: `tests/food-market.IntegrationTests/Support/ApiFactory.cs` теперь прокидывает `ConnectionStrings__Default` через `Environment.SetEnvironmentVariable` в `InitializeAsync` — `WebApplicationFactory.ConfigureAppConfiguration` опаздывает к моменту чтения строки в `Program.cs:86`, а env-var читается `CreateBuilder` сразу. Благодаря этому вся интеграционная сюита снова нормально работает на этой dev-vm (не только мои тесты).
|
||
|
||
**E2E — `tests/e2e/scenarios/stage-simple-mode.yml` + `.steps.ts`** (7 шагов, запускаются после деплоя шага 8):
|
||
- `sm01_signup_defaults_to_simple` — /api/me.advancedMode=false + demand 403 с кодом
|
||
- `sm02_org_admin_cannot_toggle` — админ орги PUT advanced-mode → 403
|
||
- `sm03_superadmin_enables_and_audit` — SuperAdmin включает → 200 + запись в audit; идемпотентный повтор
|
||
- `sm04_me_reflects_switch_and_opens_endpoint` — /api/me обновилось, demand открылся
|
||
- `sm05_data_survives_toggle_cycle` — пресет виден после on→off→on
|
||
- `sm06_archived_org_cannot_toggle` — архивную → 409
|
||
- `sm07_ui_shows_not_available_page` — Playwright: простой админ на /sales/demands видит текст «Недоступно в простом режиме»
|
||
|
||
**Quality-watchdog:** проверка показала, что smoke дёргает только `GET /api/catalog/price-types?pageSize=10` (не гейтится — только мутации). Патч смока **не требуется**.
|
||
|
||
### 2026-10-04 — Шаг 8 ✓
|
||
|
||
- `git push origin main` (Forgejo).
|
||
- `~/deploy-stage.sh` — build+push api/web, redeploy на prod-vm → `/health/ready` зелёный.
|
||
- Smoke на `https://test.admin.food-market.kz`:
|
||
- `/api/me` для SuperAdmin содержит `"advancedMode": false` ✓
|
||
- E2E сценарий `stage-simple-mode` — **7/7 passed** (включая Playwright-шаг «Недоступно в простом режиме»).
|
||
- `docs/simple-mode.md` создан: пользовательская документация с таблицей простого режима, перечнем модулей расширенного, инструкцией для SuperAdmin (API + UI), текстом ProblemDetails и ссылкой на тесты.
|
||
|
||
## Итог
|
||
|
||
**8/8 пунктов чек-листа ✓.** Код в main (dc885f3..). Стейдж обновлён, тесты зелёные (unit 6/6, integration 7/7, e2e 7/7). Данные скрытых модулей сохраняются при переключении режима — это проверено явно в integration и e2e («пресет виден после on→off→on»).
|
||
|
||
Не трогал (по правилам задачи): прод `admin.food-market.kz`, `global.json`, POS WPF, gateway nginx. Новые функции (кассовые смены, оплаты, ценники, весы, `Counterparty.IsSupplier/IsBuyer`-галочки) **не добавлял** — следующий этап.
|