Files
wildberries/templates/index.html
T
ruslan 064954e997 Redesign auto-reply settings: per-star pools, filter modes, login promo
- Support reply pools for all 5 stars (1★–5★)
- Configurable enabled stars (DB setting, not env)
- Filter mode: no_text / empty / all reviews
- New settings UI: star toggles with colors, filter radios, dynamic pool columns
- Remove review browse/export UI from main page
- Login: left panel 1/4 width, logo 140px, price 7₽/день
- Fix Jinja2 vars() → reply_pools dict

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 18:20:57 +03:00

353 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Отзывы — WB Feedback</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head>
<body>
<nav class="topbar">
<div class="topbar__inner">
<a href="{{ url_for('index') }}" class="topbar__brand">
<img src="{{ url_for('static', filename='wb.png') }}" class="topbar__logo-img" alt="WB">
<span class="topbar__name">Feedback</span>
</a>
<div class="topbar__nav">
<a href="{{ url_for('index') }}" class="topbar__link active">Отзывы</a>
<a href="{{ url_for('cabinet') }}" class="topbar__link">Кабинет</a>
{% if current_user["is_admin"] %}
<a href="{{ url_for('admin_panel') }}" class="topbar__link">Администратор</a>
{% endif %}
</div>
<div class="topbar__user">
<span class="topbar__username">{{ current_user["username"] }}</span>
{% if active_token_name %}
<span class="badge">{{ active_token_name }}</span>
{% endif %}
<a href="{{ url_for('logout') }}" class="btn-ghost">Выйти</a>
</div>
</div>
</nav>
<div class="page">
{% if error_message %}
<div class="alert alert-error">{{ error_message }}</div>
{% endif %}
{% if success_message %}
<div class="alert alert-success">{{ success_message }}</div>
{% endif %}
<!-- Автоответ -->
<div class="card auto-reply">
<div class="auto-reply__info">
<div class="auto-reply__title">
<h3>Автоответ</h3>
{% if auto_reply_enabled %}
<span class="status-dot status-dot--green"></span>
<span class="badge badge-green">Включён</span>
{% else %}
<span class="status-dot status-dot--gray"></span>
<span class="badge">Выключен</span>
{% endif %}
</div>
<p>По требованию WB — <strong>1 ответ каждые 10 минут</strong>. Очередь обрабатывается автоматически.</p>
{% if api_cooldown_seconds_left and api_cooldown_seconds_left > 0 %}
<p style="margin-top:6px;color:var(--amber)">⚠ API на паузе: ещё <span id="cooldown-counter" data-seconds="{{ api_cooldown_seconds_left }}">{{ api_cooldown_seconds_left }}</span> сек.</p>
{% endif %}
</div>
<form method="post" action="{{ url_for('auto_reply_toggle') }}" id="toggle-form">
<input type="hidden" name="enabled" value="{{ 0 if auto_reply_enabled else 1 }}">
<label class="tumbler" title="{{ 'Выключить автоответ' if auto_reply_enabled else 'Включить автоответ' }}">
<input type="checkbox" class="tumbler__input" {{ 'checked' if auto_reply_enabled else '' }} onchange="document.getElementById('toggle-form').submit()">
<span class="tumbler__track">
<span class="tumbler__thumb"></span>
</span>
</label>
</form>
</div>
<!-- Настройки автоответа -->
<div class="card">
<div class="section-header">
<h2>Настройки автоответа</h2>
</div>
<form method="post" action="{{ url_for('auto_reply_settings') }}" id="settings-form">
<!-- Звёзды -->
<div class="settings-section">
<div class="settings-label">На какие оценки отвечать</div>
<div class="star-toggles">
{% for star in [1,2,3,4,5] %}
<label class="star-toggle star-toggle--{{ star }}">
<input type="checkbox" name="stars" value="{{ star }}"
{% if star in enabled_stars %}checked{% endif %}
onchange="updateColumns()">
<span>{{ star }}★</span>
</label>
{% endfor %}
</div>
</div>
<!-- Фильтр типа отзывов -->
<div class="settings-section">
<div class="settings-label">Отвечать на отзывы</div>
<div class="filter-options">
<label class="filter-option">
<input type="radio" name="filter_mode" value="no_text" {% if filter_mode == 'no_text' %}checked{% endif %}>
<span>Без основного текста <small>(достоинства/недостатки допустимы)</small></span>
</label>
<label class="filter-option">
<input type="radio" name="filter_mode" value="empty" {% if filter_mode == 'empty' %}checked{% endif %}>
<span>Полностью пустые <small>(нет текста, достоинств и недостатков)</small></span>
</label>
<label class="filter-option">
<input type="radio" name="filter_mode" value="all" {% if filter_mode == 'all' %}checked{% endif %}>
<span>Все отзывы <small>(независимо от наличия текста)</small></span>
</label>
</div>
</div>
<!-- Динамические колонки пулов -->
<div class="settings-section">
<div class="settings-label">Шаблоны ответов</div>
<div class="pool-columns" id="pool-columns">
{% for star in [1,2,3,4,5] %}
<div class="pool-col pool-col--{{ star }}" id="pool-col-{{ star }}" {% if star not in enabled_stars %}style="display:none"{% endif %}>
<div class="pool-col__header">
<span class="star-chip star-chip--{{ star }}">{{ star }}★</span>
</div>
<div class="pool-items" id="pool-items-{{ star }}"></div>
<textarea name="pool_{{ star }}_raw" id="pool-{{ star }}-hidden" hidden>{{ reply_pools[star] }}</textarea>
<button type="button" class="btn-add-item" onclick="addPoolItem('{{ star }}')">+ Добавить ответ</button>
</div>
{% endfor %}
</div>
</div>
<div style="margin-top:20px">
<button type="submit">Сохранить настройки</button>
</div>
</form>
</div>
<!-- Очередь автоответов -->
<div class="card">
<div class="section-header">
<h2>Очередь автоответов</h2>
{% if auto_reply_queue %}
<span class="badge">{{ auto_reply_queue|length }}</span>
{% endif %}
</div>
{% if auto_reply_queue %}
<div class="logs-table-wrap">
<table class="logs-table">
<thead>
<tr>
<th>#</th>
<th>ID отзыва</th>
<th>Дата оценки</th>
<th>Оценка</th>
<th>Товар</th>
<th>Покупатель</th>
</tr>
</thead>
<tbody>
{% for item in auto_reply_queue %}
<tr>
<td>{{ loop.index }}</td>
<td><code style="font-size:0.72rem;color:var(--c-text-muted)">{{ item.get("id") or "—" }}</code></td>
<td>{{ item.get("review_created_at")|format_log_datetime if item.get("review_created_at") else "—" }}</td>
<td><span class="rating rating--{{ item.rating }}">{{ item.rating }}★</span></td>
<td>
{{ item.get("product_name") or "—" }}
{% if item.get("nm_id") %}
<span class="tag-article">#{{ item.nm_id }}</span>
{% endif %}
</td>
<td>{{ item.get("user_name") or "—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">Очередь пуста — новые отзывы будут загружены автоматически.</div>
{% endif %}
</div>
<!-- Журнал автоответов -->
<div class="card">
<div class="section-header">
<h2>Журнал автоответов</h2>
<span class="badge">последние 100</span>
</div>
{% if auto_reply_logs %}
<div class="logs-table-wrap">
<table class="logs-table">
<thead>
<tr>
<th>Дата лога</th>
<th>Дата оценки</th>
<th>Оценка</th>
<th>Товар</th>
<th>Покупатель</th>
<th>Текст отзыва</th>
<th>Статус</th>
<th>ID отзыва</th>
<th>Ответ</th>
</tr>
</thead>
<tbody>
{% for log in auto_reply_logs %}
<tr>
<td>{{ log["created_at"]|format_log_datetime }}</td>
<td>{{ log["review_created_at"]|format_log_datetime if log["review_created_at"] else "—" }}</td>
<td><span class="rating rating--{{ log['rating'] }}">{{ log["rating"] }}★</span></td>
<td>
{{ log["product_name"] or "—" }}
{% if log["nm_id"] %}
<span class="tag-article">#{{ log["nm_id"] }}</span>
{% endif %}
</td>
<td>{{ log["user_name"] or "—" }}</td>
<td>{{ log["review_text"] or "—" }}</td>
<td>
{% if log["status"] == "sent" %}
<span class="log-status--sent">✓ Отправлен</span>
{% elif log["status"] == "skipped" %}
<span class="log-status--skip">— Пропущен</span>
{% else %}
<span class="log-status--error">✗ Ошибка</span>
{% endif %}
</td>
<td><code style="font-size:0.72rem;color:var(--c-text-muted)">{{ log["review_id"] or "—" }}</code></td>
<td>{{ log["reply_text"] or "—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">Пока нет записей автоответа.</div>
{% endif %}
</div>
</div>
<!-- Нижняя навигация (мобайл) -->
<nav class="bottom-nav">
<a href="{{ url_for('index') }}" class="bottom-nav__item active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/>
<rect x="9" y="3" width="6" height="4" rx="1"/>
<line x1="9" y1="12" x2="15" y2="12"/>
<line x1="9" y1="16" x2="13" y2="16"/>
</svg>
Отзывы
</a>
<a href="{{ url_for('cabinet') }}" class="bottom-nav__item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
Кабинет
</a>
{% if current_user["is_admin"] %}
<a href="{{ url_for('admin_panel') }}" class="bottom-nav__item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
Админ
</a>
{% endif %}
</nav>
<script>
// ── Cooldown counter ───────────────────────────────────────────────
(() => {
const node = document.getElementById('cooldown-counter');
if (!node) return;
let s = parseInt(node.dataset.seconds || '0', 10);
const tick = setInterval(() => {
s--;
if (s <= 0) { clearInterval(tick); node.closest('p').remove(); return; }
node.textContent = s;
}, 1000);
})();
// ── Pool editor ────────────────────────────────────────────────────
function syncHidden(star) {
const items = document.querySelectorAll(`#pool-items-${star} .pool-item-input`);
const lines = [...items].map(i => i.value.trim()).filter(Boolean);
// inject as hidden inputs for form submission
const container = document.getElementById(`pool-items-${star}`);
container.querySelectorAll('input[name="pool_' + star + '_item"]').forEach(e => e.remove());
lines.forEach(line => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = `pool_${star}_item`;
inp.value = line;
container.appendChild(inp);
});
}
function addPoolItem(star, value = '') {
const container = document.getElementById(`pool-items-${star}`);
const row = document.createElement('div');
row.className = 'pool-item';
row.innerHTML = `<input type="text" class="pool-item-input" value="${value.replace(/"/g,'&quot;')}" placeholder="Текст ответа…"><button type="button" class="btn-delete-item" title="Удалить">✕</button>`;
row.querySelector('.pool-item-input').addEventListener('input', () => syncHidden(star));
row.querySelector('.btn-delete-item').addEventListener('click', () => { row.remove(); syncHidden(star); });
container.appendChild(row);
syncHidden(star);
}
function updateColumns() {
[1,2,3,4,5].forEach(star => {
const cb = document.querySelector(`input[name="stars"][value="${star}"]`);
const col = document.getElementById(`pool-col-${star}`);
if (col) col.style.display = cb && cb.checked ? '' : 'none';
});
}
function initPool(star) {
const hidden = document.getElementById(`pool-${star}-hidden`);
if (!hidden) return;
hidden.value.split('\n').map(l => l.trim()).filter(Boolean).forEach(line => addPoolItem(star, line));
}
document.getElementById('settings-form').addEventListener('submit', () => {
[1,2,3,4,5].forEach(syncHidden);
});
[1,2,3,4,5].forEach(initPool);
// ── API polling — reload when new log entry appears ────────────────
(() => {
let lastLogId = null;
const poll = async () => {
try {
const r = await fetch('/api/status');
if (!r.ok) return;
const data = await r.json();
if (lastLogId === null) {
lastLogId = data.last_log_id;
} else if (data.last_log_id !== lastLogId) {
window.location.reload();
}
// sync cooldown if page wasn't reloaded
const node = document.getElementById('cooldown-counter');
if (node && data.cooldown > 0) node.textContent = data.cooldown;
} catch(e) {}
};
poll();
setInterval(poll, 15000);
})();
</script>
</body>
</html>