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

18 KiB
Raw Blame History

Simple Mode — прогресс

Задача: по умолчанию у всех организаций «Простой режим» — только базовые функции розничного учёта. Включить «Расширенный режим» может только SuperAdmin и только для конкретной организации. Данные при переключении не удаляются; при повторном включении всё видно.

Старт: 2026-10-04.

Чек-лист

  • 1. Domain + миграция Organization.IsAdvancedMode (bool, default false, backfill false)
  • 2. API PUT /api/super-admin/organizations/{id}/advanced-mode + аудит + /api/me.advancedMode; org-admin → 403
  • 3. Серверная защита: [RequiresAdvancedMode] → 403 ProblemDetails на скрытых модулях
  • 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
  • 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
  • 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
  • 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
  • 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-галочки) не добавлял — следующий этап.