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>
This commit is contained in:
262
docs/plans/catalog-browser.md
Normal file
262
docs/plans/catalog-browser.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user