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>
This commit is contained in:
+163
-114
@@ -11,7 +11,7 @@
|
||||
<nav class="topbar">
|
||||
<div class="topbar__inner">
|
||||
<a href="{{ url_for('index') }}" class="topbar__brand">
|
||||
<img src="{{ url_for('static', filename='wb2.png') }}" class="topbar__logo-img" alt="WB">
|
||||
<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">
|
||||
@@ -67,59 +67,72 @@
|
||||
<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">
|
||||
|
||||
<!-- Звёзды -->
|
||||
<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>
|
||||
|
||||
<!-- Фильтр типа отзывов -->
|
||||
<!-- Фильтр — pill-кнопки -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-label">Отвечать на отзывы</div>
|
||||
<div class="filter-options">
|
||||
<label class="filter-option">
|
||||
<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>Без основного текста <small>(достоинства/недостатки допустимы)</small></span>
|
||||
<span>
|
||||
<strong>Без текста</strong>
|
||||
<small>достоинства/недостатки допустимы</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<label class="filter-pill">
|
||||
<input type="radio" name="filter_mode" value="empty" {% if filter_mode == 'empty' %}checked{% endif %}>
|
||||
<span>Полностью пустые <small>(нет текста, достоинств и недостатков)</small></span>
|
||||
<span>
|
||||
<strong>Полностью пустые</strong>
|
||||
<small>нет ни текста, ни плюсов/минусов</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<label class="filter-pill">
|
||||
<input type="radio" name="filter_mode" value="all" {% if filter_mode == 'all' %}checked{% endif %}>
|
||||
<span>Все отзывы <small>(независимо от наличия текста)</small></span>
|
||||
<span>
|
||||
<strong>Все отзывы</strong>
|
||||
<small>независимо от наличия текста</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Динамические колонки пулов -->
|
||||
<!-- Шаблоны ответов — вкладки по звёздам -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-label">Шаблоны ответов</div>
|
||||
<div class="pool-columns" id="pool-columns">
|
||||
|
||||
<!-- Вкладки звёзд -->
|
||||
<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="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 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 style="margin-top:20px">
|
||||
<button type="submit">Сохранить настройки</button>
|
||||
<div class="settings-footer">
|
||||
<button type="submit" class="btn-save">Сохранить настройки</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
@@ -180,53 +193,43 @@
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h2>Журнал автоответов</h2>
|
||||
<span class="badge">последние 100</span>
|
||||
{% if auto_reply_logs %}
|
||||
<span class="badge">{{ auto_reply_logs|length }}</span>
|
||||
{% endif %}
|
||||
</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 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>
|
||||
@@ -281,51 +284,97 @@ function startCountdown(id, onZero) {
|
||||
const cooldownCtrl = startCountdown('cooldown-counter');
|
||||
const fetchCtrl = startCountdown('fetch-counter', () => window.location.reload());
|
||||
|
||||
// ── 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);
|
||||
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);
|
||||
// ── Star tab pool editor ───────────────────────────────────────────
|
||||
const poolData = {1:[],2:[],3:[],4:[],5:[]};
|
||||
let activeStar = 5;
|
||||
|
||||
function esc(s) {
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
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 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,'"')}" 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 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 updateColumns() {
|
||||
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 cb = document.querySelector(`input[name="stars"][value="${star}"]`);
|
||||
const col = document.getElementById(`pool-col-${star}`);
|
||||
if (col) col.style.display = cb && cb.checked ? '' : 'none';
|
||||
const hidden = document.getElementById(`pool-${star}-hidden`);
|
||||
if (hidden) hidden.value = poolData[star].filter(s => s.trim()).join('\n');
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
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', () => {
|
||||
[1,2,3,4,5].forEach(syncHidden);
|
||||
});
|
||||
|
||||
[1,2,3,4,5].forEach(initPool);
|
||||
document.getElementById('settings-form').addEventListener('submit', serializeForSubmit);
|
||||
document.getElementById('btn-add-template')?.addEventListener('click', addTemplate);
|
||||
initPools();
|
||||
|
||||
// ── API polling ────────────────────────────────────────────────────
|
||||
(() => {
|
||||
|
||||
Reference in New Issue
Block a user