Files
evo-sync/docs/plans/catalog-browser.md
mguschin 9aeef73b10 feat: release v1.8.0 — connections dashboard, VK OAuth, sync config, catalog browser
- Connections dashboard with add/remove flow and background health checks
- VK OAuth connection with profile info and health monitoring
- Sync configuration page with master toggle and filter summary
- Catalog browser with store/group/product tables, filter management, CSV export
- Alembic migrations for all new tables
- run/read_config.py for shell sync script DB integration
- CHANGELOG.md updated for v1.8.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:08:19 +03:00

12 KiB
Raw Blame History

Catalog Browser with Filter Management & CSV Export

Context

Users need to browse their Evotor catalog (stores, groups, products) in a table view, manage sync whitelist/blacklist rules inline, and export data to CSV.

This feature replaces the separate /sync/stores, /sync/groups, /sync/products pages from the sync-configuration plan. The catalog browser becomes the unified place for both viewing data and managing filter rules.

Data is cached in DB with a refresh mechanism — not fetched live on every page load.

Data Model

Catalog Cache Tables

tablename: "cached_stores"
- id (Integer, PK)
- user_id (Integer, FK users.id CASCADE)
- evotor_id (String 255)               # Evotor UUID
- name (String 255)
- address (String 500, nullable)
- fetched_at (DateTime)                 # when this snapshot was taken

UniqueConstraint: (user_id, evotor_id)
Index: user_id
tablename: "cached_groups"
- id (Integer, PK)
- user_id (Integer, FK users.id CASCADE)
- evotor_id (String 255)               # Evotor UUID
- store_evotor_id (String 255)         # parent store UUID
- name (String 255)
- fetched_at (DateTime)

UniqueConstraint: (user_id, evotor_id)
Index: (user_id, store_evotor_id)
tablename: "cached_products"
- id (Integer, PK)
- user_id (Integer, FK users.id CASCADE)
- evotor_id (String 255)               # Evotor UUID
- store_evotor_id (String 255)         # parent store UUID
- group_evotor_id (String 255, nullable)  # parent group UUID
- name (String 255)
- price (Numeric(12,2), nullable)
- quantity (Numeric(12,3), nullable)
- measure_name (String 20, nullable)
- article_number (String 100, nullable)
- allow_to_sell (Boolean, nullable)
- fetched_at (DateTime)

UniqueConstraint: (user_id, evotor_id)
Index: (user_id, store_evotor_id, group_evotor_id)

SyncFilter (from sync-configuration plan, unchanged)

tablename: "sync_filters"
- sync_config_id, entity_type, entity_id, entity_name, filter_mode, parent_entity_id

The catalog browser reads from cache tables for display and from sync_filters for the current filter state of each entity.

Cache Refresh

web/evotor_api.py gets a new function:

async def refresh_catalog_cache(user_id: int, access_token: str, db: Session):
    """Fetch all stores, groups, products from Evotor API and upsert into cache tables."""

Triggered by:

  • Manual "Обновить" button on the catalog page
  • Background job (optional, can reuse health_checker interval or separate setting)
  • First visit to catalog if cache is empty

Plan

1. New Models — web/models.py

Add CachedStore, CachedGroup, CachedProduct models as described above.

2. Alembic Migration

Create cached_stores, cached_groups, cached_products tables.

3. Evotor API Helper — web/evotor_api.py

Extend with:

async def fetch_stores(access_token: str) -> list[dict]
async def fetch_groups(access_token: str, store_id: str) -> list[dict]
async def fetch_products(access_token: str, store_id: str) -> list[dict]
async def refresh_catalog_cache(user_id: int, access_token: str, db: Session)

refresh_catalog_cache does:

  1. Fetch all stores
  2. For each store, fetch groups and products
  3. Upsert into cache tables (delete old rows for user, insert fresh)
  4. Update fetched_at timestamps

4. Catalog Route — web/routes/catalog.py (new)

GET /catalog — Stores table. Requires auth + Evotor connection.

  • Reads cached_stores for user
  • If cache is empty, triggers refresh
  • Shows table with columns: Название, Адрес, Статус фильтра, Действия
  • Each row shows the store's current SyncFilter state (included/excluded/no rule)
  • Link to drill into groups for each store
  • "Обновить каталог" button, "Экспорт CSV" button, back link

GET /catalog/groups?store_id=UUID — Groups table for a store.

  • Reads cached_groups filtered by store_evotor_id
  • Table columns: Название, Статус фильтра, Кол-во товаров, Действия
  • Each row shows group's SyncFilter state
  • Link to drill into products for each group
  • "Экспорт CSV" button, back to stores

GET /catalog/products?store_id=UUID&group_id=UUID — Products table for a group.

  • Reads cached_products filtered by store_evotor_id and group_evotor_id
  • Table columns: Название, Артикул, Цена, Кол-во, Ед. изм., В продаже, Статус фильтра, Действия
  • Each row shows product's SyncFilter state
  • "Экспорт CSV" button, back to groups

GET /catalog/products?store_id=UUID — All products for a store (no group filter).

  • Same table, but shows all products in the store with a "Группа" column added

POST /catalog/filter — Toggle filter for an entity.

  • Body: entity_type, entity_id, entity_name, filter_mode (include/exclude/none), parent_entity_id
  • Creates, updates, or deletes the SyncFilter row
  • Redirects back to the referring page

POST /catalog/refresh — Manual cache refresh.

  • Calls refresh_catalog_cache()
  • Redirects back to /catalog

GET /catalog/export?type=stores|groups|products&store_id=UUID&group_id=UUID — CSV export.

  • Reads from cache tables
  • Returns StreamingResponse with text/csv content type and Content-Disposition: attachment
  • Filename: {type}_{date}.csv

5. Templates

web/templates/catalog_stores.html — Stores table:

┌──────────────────────────────────────────────────────────────┐
│  Каталог                        [Обновить]  [Экспорт CSV]   │
│  Последнее обновление: 06.03.2026 14:30                      │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Магазины                                                    │
│  ┌──────────────────────────────────────────────────────┐    │
│  │ Название        │ Адрес        │ Фильтр   │         │    │
│  ├──────────────────────────────────────────────────────┤    │
│  │ Чайная           │ ул. Мира, 1  │ ✓ Вкл    │ [→][▼] │    │
│  │ Склад            │ —            │ ✗ Выкл   │ [→][▼] │    │
│  │ Точка 2          │ ул. Мира, 5  │ — Нет    │ [→][▼] │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  [→] = перейти к группам                                     │
│  [▼] = dropdown: Включить / Исключить / Убрать правило      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Filter status column shows:

  • ✓ Включено (green badge) — entity has an "include" rule
  • ✗ Исключено (red badge) — entity has an "exclude" rule
  • — Нет правила (grey badge) — no filter rule (follows default behavior)

Actions column per row:

  • Link icon → drill into children (groups for stores, products for groups)
  • Dropdown button with filter actions: "Включить в синхронизацию" / "Исключить из синхронизации" / "Убрать правило". Each is a small POST form to /catalog/filter.

web/templates/catalog_groups.html — Groups table:

  • Breadcrumb: Каталог > {Store name} > Группы
  • Same table pattern, columns: Название, Кол-во товаров, Фильтр, Действия
  • Drill-down link to products per group

web/templates/catalog_products.html — Products table:

  • Breadcrumb: Каталог > {Store name} > {Group name} > Товары
  • Columns: Название, Артикул, Цена, Кол-во, Ед. изм., В продаже, Фильтр, Действия
  • "В продаже" column: green check / red cross based on allow_to_sell

All tables use Bootstrap table styling (table table-striped table-hover) with responsive wrapper.

6. CSV Export Format

Stores CSV:

Название,Адрес,ID,Фильтр
Чайная,"ул. Мира, 1",uuid-123,Включено

Groups CSV:

Магазин,Название,ID,Фильтр
Чайная,Белый чай,uuid-456,Включено

Products CSV:

Магазин,Группа,Название,Артикул,Цена,Количество,Ед. измерения,В продаже,ID,Фильтр
Чайная,Белый чай,Бай Му Дань,1005,350.00,180.0,г,Да,uuid-789,Включено

UTF-8 with BOM (\ufeff) for Excel compatibility. Delimiter: comma.

7. Update Sync Configuration Plan

The /sync page links to /catalog instead of separate filter pages:

  • "Настроить фильтры" button → /catalog
  • Filter summary on /sync reads from SyncFilter table (unchanged)
  • Remove /sync/stores, /sync/groups, /sync/products routes from sync-configuration plan — replaced by catalog browser

8. Navbar / Navigation

Add "Каталог" link to navbar for logged-in users. Order: Подключения → Каталог → Синхронизация → Личный кабинет → Выход.

9. Register Route — web/main.py

from web.routes import catalog
app.include_router(catalog.router)

Files Summary

File Action
web/models.py Modify — add CachedStore, CachedGroup, CachedProduct
web/evotor_api.py Create — API fetch + cache refresh functions
web/routes/catalog.py Create — catalog routes (tables, filter toggle, refresh, CSV export)
web/templates/catalog_stores.html Create — stores table
web/templates/catalog_groups.html Create — groups table
web/templates/catalog_products.html Create — products table
web/templates/base.html Modify — add "Каталог" nav link
web/main.py Modify — register catalog router
docs/plans/sync-configuration.md Update — remove /sync/stores,groups,products; link to /catalog
Alembic migration Create — cache tables

Verification

  1. Run alembic upgrade head
  2. Visit /catalog without Evotor connection → warning to connect first
  3. Connect Evotor, visit /catalog → triggers first cache refresh, shows stores table
  4. Click store → shows groups table with group names from Evotor
  5. Click group → shows products table with full product details
  6. Toggle filter on a product → badge changes, SyncFilter row created in DB
  7. Go to /sync → filter summary reflects the change
  8. Click "Экспорт CSV" on products page → downloads CSV, opens correctly in Excel
  9. Click "Обновить каталог" → re-fetches from Evotor API, updates cache
  10. Verify breadcrumb navigation works correctly through the hierarchy