Files
wildberries/templates/index.html
T
ruslan 93be1e8089 Redesign settings UI and log entries, add wb3 logo
- Settings: tab per star with mini-toggle + template count badge
- Templates: full-width textarea per template (not cramped columns)
- Filter mode: pill-cards instead of raw radio buttons
- Save button aligned right with border-top separator
- Log: card-based entries (rating pill, meta row, review/reply blocks)
- wb3.png logo, 160px on login, 40px in topbar with ring shadow
- Fix CSS variable names (--c-* → --line/--card/--text/--muted)

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

405 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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='wb3.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">
<div class="section-header">
<h2>Настройки автоответа</h2>
<form method="post" action="{{ url_for('auto_reply_toggle') }}">
<input type="hidden" name="enabled" value="{{ 0 if auto_reply_enabled else 1 }}">
{% if auto_reply_enabled %}
<button type="submit" class="btn-stop">&#9646;&#9646; Стоп</button>
{% else %}
<button type="submit" class="btn-start">&#9654; Старт</button>
{% endif %}
</form>
</div>
{% if auto_reply_enabled %}
<div class="autoreply-status">
<span class="status-dot status-dot--green"></span>
<span>Автоответ активен</span>
{% if api_cooldown_seconds_left and api_cooldown_seconds_left > 0 %}
&nbsp;·&nbsp; Следующий ответ через <span id="cooldown-counter" data-seconds="{{ api_cooldown_seconds_left }}">{{ api_cooldown_seconds_left }}</span> сек.
{% endif %}
</div>
{% endif %}
<form method="post" action="{{ url_for('auto_reply_settings') }}" id="settings-form">
<fieldset {% if auto_reply_enabled %}disabled{% endif %} style="border:none;padding:0;margin:0">
<!-- Фильтр — pill-кнопки -->
<div class="settings-section">
<div class="settings-label">Отвечать на отзывы</div>
<div class="filter-pills">
<label class="filter-pill">
<input type="radio" name="filter_mode" value="no_text" {% if filter_mode == 'no_text' %}checked{% endif %}>
<span>
<strong>Без текста</strong>
<small>достоинства/недостатки допустимы</small>
</span>
</label>
<label class="filter-pill">
<input type="radio" name="filter_mode" value="empty" {% if filter_mode == 'empty' %}checked{% endif %}>
<span>
<strong>Полностью пустые</strong>
<small>нет ни текста, ни плюсов/минусов</small>
</span>
</label>
<label class="filter-pill">
<input type="radio" name="filter_mode" value="all" {% if filter_mode == 'all' %}checked{% endif %}>
<span>
<strong>Все отзывы</strong>
<small>независимо от наличия текста</small>
</span>
</label>
</div>
</div>
<!-- Шаблоны ответов — вкладки по звёздам -->
<div class="settings-section">
<div class="settings-label">Шаблоны ответов</div>
<!-- Вкладки звёзд -->
<div class="star-tabs" id="star-tabs">
{% set star_labels = {1: 'Плохо', 2: 'Не понравилось', 3: 'Нормально', 4: 'Хорошо', 5: 'Отлично'} %}
{% for star in [1,2,3,4,5] %}
<div class="star-tab star-tab--{{ star }} {% if star in enabled_stars %}star-tab--on{% endif %}"
data-star="{{ star }}" onclick="setActiveStar({{ star }})">
<span class="star-tab__label">{{ star }}★</span>
<span class="star-tab__count" id="tab-count-{{ star }}">0</span>
<label class="star-tab__toggle" onclick="event.stopPropagation()" title="Включить/выключить">
<input type="checkbox" name="stars" value="{{ star }}"
id="star-cb-{{ star }}"
{% if star in enabled_stars %}checked{% endif %}
onchange="onStarToggle({{ star }})">
<span class="star-tab__toggle-track"></span>
</label>
</div>
{% endfor %}
</div>
<!-- Панель редактирования активной звезды -->
<div class="star-panel" id="star-panel">
<div class="star-panel__header" id="star-panel-header"></div>
<div class="pool-panel-items" id="pool-panel-items"></div>
<button type="button" class="btn-add-template" id="btn-add-template">+ Добавить шаблон</button>
</div>
<!-- Скрытые textarea со значениями пулов для всех звёзд -->
{% for star in [1,2,3,4,5] %}
<textarea name="pool_{{ star }}_raw" id="pool-{{ star }}-hidden" hidden>{{ reply_pools[star] }}</textarea>
{% endfor %}
</div>
<div class="settings-footer">
<button type="submit" class="btn-save">Сохранить настройки</button>
</div>
</fieldset>
</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 %}
{% if auto_reply_enabled and next_fetch_seconds_left > 0 %}
<div class="empty-state">Очередь подгрузится через <span id="fetch-counter" data-seconds="{{ next_fetch_seconds_left }}">{{ next_fetch_seconds_left }}</span> сек.</div>
{% elif auto_reply_enabled %}
<div class="empty-state">Загрузка очереди…</div>
{% else %}
<div class="empty-state">Нажмите «Старт» для запуска автоответов.</div>
{% endif %}
{% endif %}
</div>
<!-- Журнал автоответов -->
<div class="card">
<div class="section-header">
<h2>Журнал автоответов</h2>
{% if auto_reply_logs %}
<span class="badge">{{ auto_reply_logs|length }}</span>
{% endif %}
</div>
{% if auto_reply_logs %}
<div class="log-list">
{% for log in auto_reply_logs %}
<div class="log-entry log-entry--{{ log['status'] }}">
<div class="log-entry__left">
<span class="log-entry__rating rating--{{ log['rating'] }}">{{ log['rating'] }}★</span>
<div class="log-entry__status">
{% 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 %}
</div>
</div>
<div class="log-entry__body">
<div class="log-entry__meta">
<span class="log-entry__product">{{ log['product_name'] or '—' }}{% if log['nm_id'] %} <span class="tag-article">#{{ log['nm_id'] }}</span>{% endif %}</span>
<span class="log-entry__buyer">{{ log['user_name'] or '—' }}</span>
<span class="log-entry__time">{{ log['created_at']|format_log_datetime }}</span>
</div>
{% if log['review_text'] %}
<div class="log-entry__review">{{ log['review_text'] }}</div>
{% endif %}
{% if log['reply_text'] %}
<div class="log-entry__reply">
<span class="log-entry__reply-ico"></span>{{ log['reply_text'] }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</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>
// ── Tick-down helper ───────────────────────────────────────────────
function startCountdown(id, onZero) {
const node = document.getElementById(id);
if (!node) return;
let s = parseInt(node.dataset.seconds || '0', 10);
const tick = setInterval(() => {
s--;
if (s <= 0) { clearInterval(tick); if (onZero) onZero(); else node.textContent = '0'; return; }
node.textContent = s;
}, 1000);
return { update: (v) => { s = v; node.textContent = v; } };
}
const cooldownCtrl = startCountdown('cooldown-counter');
const fetchCtrl = startCountdown('fetch-counter', () => window.location.reload());
// ── Star tab pool editor ───────────────────────────────────────────
const poolData = {1:[],2:[],3:[],4:[],5:[]};
let activeStar = 5;
function esc(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function updateTabCount(star) {
const el = document.getElementById(`tab-count-${star}`);
if (el) el.textContent = poolData[star].length;
}
function onStarToggle(star) {
const cb = document.getElementById(`star-cb-${star}`);
const tab = document.querySelector(`.star-tab[data-star="${star}"]`);
if (tab) tab.classList.toggle('star-tab--on', cb.checked);
}
function setActiveStar(star) {
activeStar = star;
document.querySelectorAll('.star-tab').forEach(t =>
t.classList.toggle('star-tab--active', +t.dataset.star === star));
renderPanel();
}
function renderPanel() {
const COLORS = {1:'#EF4444',2:'#F97316',3:'#EAB308',4:'#3B82F6',5:'#22C55E'};
const LABELS = {1:'1★ — Плохо',2:'2★ — Не понравилось',3:'3★ — Нормально',4:'4★ — Хорошо',5:'5★ — Отлично'};
const header = document.getElementById('star-panel-header');
if (header) {
header.innerHTML = `<span style="color:${COLORS[activeStar]};font-weight:700;font-size:15px">${LABELS[activeStar]}</span>
<span style="color:var(--c-text-muted);font-size:13px">${poolData[activeStar].length ? poolData[activeStar].length + ' шаблон(а/ов)' : 'нет шаблонов'}</span>`;
}
const container = document.getElementById('pool-panel-items');
container.innerHTML = '';
poolData[activeStar].forEach((text, idx) => {
const row = document.createElement('div');
row.className = 'pool-template-row';
row.innerHTML = `<span class="pool-template-num">${idx+1}</span>
<textarea class="pool-template-input" rows="3" placeholder="Текст ответа для ${activeStar}★…">${esc(text)}</textarea>
<button type="button" class="pool-template-del" title="Удалить">✕</button>`;
const ta = row.querySelector('textarea');
ta.addEventListener('input', () => { poolData[activeStar][idx] = ta.value; updateTabCount(activeStar); renderPanelHeader(); });
row.querySelector('.pool-template-del').addEventListener('click', () => {
poolData[activeStar].splice(idx, 1);
updateTabCount(activeStar);
renderPanel();
});
container.appendChild(row);
});
}
function renderPanelHeader() {
const LABELS = {1:'1★ — Плохо',2:'2★ — Не понравилось',3:'3★ — Нормально',4:'4★ — Хорошо',5:'5★ — Отлично'};
const COLORS = {1:'#EF4444',2:'#F97316',3:'#EAB308',4:'#3B82F6',5:'#22C55E'};
const header = document.getElementById('star-panel-header');
if (header) {
header.innerHTML = `<span style="color:${COLORS[activeStar]};font-weight:700;font-size:15px">${LABELS[activeStar]}</span>
<span style="color:var(--c-text-muted);font-size:13px">${poolData[activeStar].length ? poolData[activeStar].length + ' шаблон(а/ов)' : 'нет шаблонов'}</span>`;
}
}
function addTemplate() {
poolData[activeStar].push('');
updateTabCount(activeStar);
renderPanel();
const inputs = document.querySelectorAll('.pool-template-input');
if (inputs.length) { inputs[inputs.length-1].focus(); }
}
function serializeForSubmit() {
[1,2,3,4,5].forEach(star => {
const hidden = document.getElementById(`pool-${star}-hidden`);
if (hidden) hidden.value = poolData[star].filter(s => s.trim()).join('\n');
});
}
function initPools() {
[1,2,3,4,5].forEach(star => {
const hidden = document.getElementById(`pool-${star}-hidden`);
if (hidden) poolData[star] = hidden.value.split('\n').map(l => l.trim()).filter(Boolean);
updateTabCount(star);
});
const firstOn = [5,4,3,2,1].find(s => document.getElementById(`star-cb-${s}`)?.checked) || 5;
setActiveStar(firstOn);
}
document.getElementById('settings-form').addEventListener('submit', serializeForSubmit);
document.getElementById('btn-add-template')?.addEventListener('click', addTemplate);
initPools();
// ── API polling ────────────────────────────────────────────────────
(() => {
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();
}
if (cooldownCtrl && data.cooldown > 0) cooldownCtrl.update(data.cooldown);
if (fetchCtrl && data.next_fetch_seconds > 0) fetchCtrl.update(data.next_fetch_seconds);
if (data.next_fetch_seconds === 0 && data.queue_len === 0 && data.auto_reply_enabled) {
window.location.reload();
}
} catch(e) {}
};
poll();
setInterval(poll, 10000);
})();
</script>
</body>
</html>