food-market/docs/simple-mode-progress.md
nns 8572875e7f
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
docs(s29): simple-mode user guide + стейдж задеплоен + e2e 7/7 ✓ (шаг 8/8)
- 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>
2026-10-04 12:11:03 +05:00

197 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`-галочки) **не добавлял** — следующий этап.