feat: Evotor + VK catalog sync, connections, and store/group filters

- Evotor catalog: background Celery task syncing stores/groups/products
  from Evotor API; UI pages with per-store and per-group sync toggles
- VK connection: manual token + group ID entry with inline test button
- Evotor connection: inline test button (calls /stores)
- VK catalog: background task syncing VK Market albums and products;
  separate catalog UI at /vk-catalog/albums
- SyncFilter extended to support entity_type=group with parent_entity_id
- Migration 0004: vk_cached_albums + vk_cached_products tables
- Beat schedule updated to run both refresh_catalog and refresh_vk_catalog
- README updated with new schema, routes, tasks, and config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mguschin
2026-05-01 18:09:11 +03:00
parent 7a06045bef
commit 796cf49ff9
18 changed files with 1716 additions and 47 deletions

View File

@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Альбомы ВК — ЭВОСИНК{% endblock %}
{% block content %}
<div class="d-flex justify-between align-center mb-3">
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-badge-vr me-2"></i>Каталог ВКонтакте — Альбомы</h1>
<span class="text-muted small">Всего: {{ albums | length }}</span>
</div>
<article class="card">
{% if not vk_conn %}
<div class="text-center py-5 text-muted">
<i class="bi bi-plug" style="font-size:2rem;"></i>
<p class="mt-2">ВКонтакте не подключён.<br><a href="/connections">Перейти к подключениям</a></p>
</div>
{% elif albums %}
<div class="table-scroll">
<table class="align-middle">
<thead>
<tr>
<th>Название</th>
<th>Товаров</th>
<th>ID</th>
<th>Обновлено</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in albums %}
<tr>
<td><i class="bi bi-collection me-1 text-muted"></i> <strong>{{ a.title }}</strong></td>
<td class="text-muted">{{ a.count if a.count is not none else '—' }}</td>
<td class="text-muted small">{{ a.album_id }}</td>
<td class="text-muted small">{{ a.fetched_at | datefmt }}</td>
<td>
<a href="/vk-catalog/albums/{{ a.album_id }}/products" role="button" class="outline sm">
<i class="bi bi-box-seam"></i> Товары
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5 text-muted">
<i class="bi bi-collection" style="font-size:2rem;"></i>
<p class="mt-2">Альбомы ещё не загружены.<br>Синхронизация выполняется каждые {{ refresh_interval }} сек. автоматически.</p>
</div>
{% endif %}
</article>
{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Товары ВК — {{ album.title }} — ЭВОСИНК{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li><a href="/vk-catalog/albums">Альбомы ВК</a></li>
<li>{{ album.title }}</li>
</ol>
</nav>
<div class="d-flex justify-between align-center mb-3">
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-box-seam me-2"></i>{{ album.title }}</h1>
<span class="text-muted small">Всего: {{ products | length }}</span>
</div>
<article class="card">
{% if products %}
<div class="table-scroll">
<table class="align-middle">
<thead>
<tr>
<th></th>
<th>Название</th>
<th>Цена</th>
<th>Статус</th>
<th>ID</th>
<th>Обновлено</th>
</tr>
</thead>
<tbody>
{% for p in products %}
<tr>
<td style="width:48px;">
{% if p.thumb_url %}
<img src="{{ p.thumb_url }}" alt="" style="width:40px;height:40px;object-fit:cover;border-radius:4px;">
{% else %}
<span class="text-muted"><i class="bi bi-image" style="font-size:1.5rem;"></i></span>
{% endif %}
</td>
<td>
<strong>{{ p.name }}</strong>
{% if p.description %}
<br><span class="text-muted small">{{ p.description[:80] }}{% if p.description|length > 80 %}…{% endif %}</span>
{% endif %}
</td>
<td class="text-muted">
{% if p.price is not none %}{{ p.price | price }}{% else %}—{% endif %}
</td>
<td>
{% if p.availability == 0 %}
<span class="badge badge-success">В наличии</span>
{% elif p.availability == 1 %}
<span class="badge badge-secondary">Удалён</span>
{% elif p.availability == 2 %}
<span class="badge badge-warning">Недоступен</span>
{% else %}
<span class="text-muted small"></span>
{% endif %}
</td>
<td class="text-muted small">{{ p.vk_product_id }}</td>
<td class="text-muted small">{{ p.fetched_at | datefmt }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5 text-muted">
<i class="bi bi-box-seam" style="font-size:2rem;"></i>
<p class="mt-2">Товары в этом альбоме не найдены.</p>
</div>
{% endif %}
</article>
{% endblock %}