# 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: ```python 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: ```python 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` ```python 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