Files
wildberries/templates/index.html
T
2026-05-15 21:35:39 +03:00

432 lines
21 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">
</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 tokens %}
<div class="store-picker">
<button type="button" class="store-picker__btn" onclick="this.closest('.store-picker').classList.toggle('open')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
{{ active_token_name or 'Магазин не выбран' }}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="store-picker__dropdown">
{% for t in tokens %}
<form method="post" action="{{ url_for('cabinet') }}">
<input type="hidden" name="cabinet_action" value="select">
<input type="hidden" name="token_id" value="{{ t.id }}">
<input type="hidden" name="next" value="index">
<button type="submit" class="store-picker__item {% if t.id == active_token_id %}store-picker__item--active{% endif %}">
{{ t.name }}
{% if t.id == active_token_id %}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>{% endif %}
</button>
</form>
{% endfor %}
<div class="store-picker__sep"></div>
<a href="{{ url_for('cabinet') }}" class="store-picker__item">Управление магазинами →</a>
</div>
</div>
{% 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);
})();
// ── Store picker close on outside click ───────────────────
document.addEventListener('click', e => {
if (!e.target.closest('.store-picker')) {
document.querySelectorAll('.store-picker.open').forEach(el => el.classList.remove('open'));
}
});
</script>
</body>
</html>