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:
+308
-15
@@ -129,14 +129,14 @@ a:hover { color: var(--blue-dark); text-decoration: underline; }
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.topbar__logo-img {
|
.topbar__logo-img {
|
||||||
width: 34px;
|
width: 40px;
|
||||||
height: 34px;
|
height: 40px;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 3px;
|
padding: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(203,17,171,0.25);
|
box-shadow: 0 2px 10px rgba(203,17,171,0.3), 0 0 0 2px rgba(203,17,171,0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar__name {
|
.topbar__name {
|
||||||
@@ -931,14 +931,14 @@ textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-promo__logo {
|
.login-promo__logo {
|
||||||
width: 140px;
|
width: 160px;
|
||||||
height: 140px;
|
height: 160px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 20px;
|
border-radius: 24px;
|
||||||
background: white;
|
background: #fff;
|
||||||
padding: 8px;
|
padding: 16px;
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
|
box-shadow: 0 12px 40px rgba(0,0,0,0.35), 0 0 0 4px rgba(255,255,255,0.15);
|
||||||
margin-bottom: 28px;
|
margin-bottom: 32px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,7 +1037,7 @@ textarea:focus {
|
|||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
color: var(--c-text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Auth card ──────────────────────────────────────────────────── */
|
/* ─── Auth card ──────────────────────────────────────────────────── */
|
||||||
@@ -1407,9 +1407,9 @@ textarea:focus {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--c-text-muted);
|
color: var(--muted);
|
||||||
padding: 8px 0 12px;
|
padding: 8px 0 12px;
|
||||||
border-bottom: 1px solid var(--c-border);
|
border-bottom: 1px solid var(--line);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1531,3 +1531,296 @@ fieldset:disabled button[type="submit"] {
|
|||||||
.star-chip--1 { background: #FEE2E2; color: #EF4444; }
|
.star-chip--1 { background: #FEE2E2; color: #EF4444; }
|
||||||
.star-chip--2 { background: #FFEDD5; color: #F97316; }
|
.star-chip--2 { background: #FFEDD5; color: #F97316; }
|
||||||
.star-chip--3 { background: #FEF9C3; color: #CA8A04; }
|
.star-chip--3 { background: #FEF9C3; color: #CA8A04; }
|
||||||
|
|
||||||
|
/* ── Filter pills ────────────────────────────────────────── */
|
||||||
|
.filter-pills {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.filter-pill {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1 1 160px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
.filter-pill input { display: none; }
|
||||||
|
.filter-pill span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
background: var(--card);
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.filter-pill span strong { font-size: 13px; font-weight: 600; }
|
||||||
|
.filter-pill span small { font-size: 11px; color: var(--muted); }
|
||||||
|
.filter-pill input:checked + span {
|
||||||
|
border-color: var(--wb);
|
||||||
|
background: var(--wb-light);
|
||||||
|
}
|
||||||
|
.filter-pill input:checked + span strong { color: var(--wb); }
|
||||||
|
|
||||||
|
/* ── Star tabs ───────────────────────────────────────────── */
|
||||||
|
.star-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.star-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: var(--r) var(--r) 0 0;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-bottom: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--card);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity .15s, background .15s, border-color .15s;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
bottom: -2px;
|
||||||
|
}
|
||||||
|
.star-tab.star-tab--on { opacity: 1; }
|
||||||
|
.star-tab.star-tab--active {
|
||||||
|
background: var(--bg);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.star-tab--1.star-tab--active { border-color: #EF4444; }
|
||||||
|
.star-tab--2.star-tab--active { border-color: #F97316; }
|
||||||
|
.star-tab--3.star-tab--active { border-color: #EAB308; }
|
||||||
|
.star-tab--4.star-tab--active { border-color: #3B82F6; }
|
||||||
|
.star-tab--5.star-tab--active { border-color: #22C55E; }
|
||||||
|
.star-tab__label { font-weight: 700; font-size: 15px; }
|
||||||
|
.star-tab--1 .star-tab__label { color: #EF4444; }
|
||||||
|
.star-tab--2 .star-tab__label { color: #F97316; }
|
||||||
|
.star-tab--3 .star-tab__label { color: #EAB308; }
|
||||||
|
.star-tab--4 .star-tab__label { color: #3B82F6; }
|
||||||
|
.star-tab--5 .star-tab__label { color: #22C55E; }
|
||||||
|
.star-tab__count {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: var(--line);
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
/* mini toggle inside tab */
|
||||||
|
.star-tab__toggle { cursor: pointer; display: flex; align-items: center; }
|
||||||
|
.star-tab__toggle input { display: none; }
|
||||||
|
.star-tab__toggle-track {
|
||||||
|
display: inline-block;
|
||||||
|
width: 28px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--line);
|
||||||
|
position: relative;
|
||||||
|
transition: background .15s;
|
||||||
|
}
|
||||||
|
.star-tab__toggle-track::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px; left: 2px;
|
||||||
|
width: 12px; height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
transition: transform .15s;
|
||||||
|
}
|
||||||
|
.star-tab__toggle input:checked ~ .star-tab__toggle-track { background: #22C55E; }
|
||||||
|
.star-tab__toggle input:checked ~ .star-tab__toggle-track::after { transform: translateX(12px); }
|
||||||
|
|
||||||
|
/* ── Star panel ──────────────────────────────────────────── */
|
||||||
|
.star-panel {
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 0 var(--r) var(--r) var(--r);
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.star-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.pool-panel-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.pool-template-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28px 1fr 36px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.pool-template-num {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--line);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.pool-template-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1.5px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color .15s;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.pool-template-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--wb);
|
||||||
|
}
|
||||||
|
.pool-template-del {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--red-bg);
|
||||||
|
color: var(--red);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
transition: background .15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.pool-template-del:hover { background: #fecaca; }
|
||||||
|
.btn-add-template {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 9px 16px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
border: 1.5px dashed var(--line);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, color .15s;
|
||||||
|
}
|
||||||
|
.btn-add-template:hover { border-color: var(--wb); color: var(--wb); }
|
||||||
|
|
||||||
|
/* ── Save button ─────────────────────────────────────────── */
|
||||||
|
.settings-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.btn-save {
|
||||||
|
padding: 10px 28px;
|
||||||
|
background: var(--wb);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--r);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
.btn-save:hover { opacity: 0.85; }
|
||||||
|
fieldset:disabled .btn-save { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
fieldset:disabled .pool-template-input,
|
||||||
|
fieldset:disabled .pool-template-del,
|
||||||
|
fieldset:disabled .btn-add-template,
|
||||||
|
fieldset:disabled .star-tab,
|
||||||
|
fieldset:disabled .filter-pill { pointer-events: none; opacity: 0.5; }
|
||||||
|
|
||||||
|
/* ── Log entries ─────────────────────────────────────────── */
|
||||||
|
.log-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.log-entry:last-child { border-bottom: none; }
|
||||||
|
.log-entry__left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 52px;
|
||||||
|
}
|
||||||
|
.log-entry__rating {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.log-entry__status { font-size: 11px; text-align: center; white-space: nowrap; }
|
||||||
|
.log-entry__body { flex: 1; min-width: 0; }
|
||||||
|
.log-entry__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.log-entry__product { font-size: 13px; font-weight: 600; color: var(--text); }
|
||||||
|
.log-entry__buyer { font-size: 12px; color: var(--muted); }
|
||||||
|
.log-entry__time { font-size: 11px; color: var(--muted); margin-left: auto; }
|
||||||
|
.log-entry__review {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--line);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.log-entry__reply {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--wb-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--wb);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.log-entry__reply-ico {
|
||||||
|
color: var(--wb);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="topbar__inner">
|
<div class="topbar__inner">
|
||||||
<a href="{{ url_for('index') }}" class="topbar__brand">
|
<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>
|
<span class="topbar__name">Feedback</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="topbar__nav">
|
<div class="topbar__nav">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="topbar__inner">
|
<div class="topbar__inner">
|
||||||
<a href="{{ url_for('index') }}" class="topbar__brand">
|
<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>
|
<span class="topbar__name">Feedback</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="topbar__nav">
|
<div class="topbar__nav">
|
||||||
|
|||||||
+163
-114
@@ -11,7 +11,7 @@
|
|||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="topbar__inner">
|
<div class="topbar__inner">
|
||||||
<a href="{{ url_for('index') }}" class="topbar__brand">
|
<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>
|
<span class="topbar__name">Feedback</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="topbar__nav">
|
<div class="topbar__nav">
|
||||||
@@ -67,59 +67,72 @@
|
|||||||
<form method="post" action="{{ url_for('auto_reply_settings') }}" id="settings-form">
|
<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">
|
<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="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-section">
|
||||||
<div class="settings-label">Отвечать на отзывы</div>
|
<div class="settings-label">Отвечать на отзывы</div>
|
||||||
<div class="filter-options">
|
<div class="filter-pills">
|
||||||
<label class="filter-option">
|
<label class="filter-pill">
|
||||||
<input type="radio" name="filter_mode" value="no_text" {% if filter_mode == 'no_text' %}checked{% endif %}>
|
<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>
|
||||||
<label class="filter-option">
|
<label class="filter-pill">
|
||||||
<input type="radio" name="filter_mode" value="empty" {% if filter_mode == 'empty' %}checked{% endif %}>
|
<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>
|
||||||
<label class="filter-option">
|
<label class="filter-pill">
|
||||||
<input type="radio" name="filter_mode" value="all" {% if filter_mode == 'all' %}checked{% endif %}>
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Динамические колонки пулов -->
|
<!-- Шаблоны ответов — вкладки по звёздам -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="settings-label">Шаблоны ответов</div>
|
<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] %}
|
{% 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="star-tab star-tab--{{ star }} {% if star in enabled_stars %}star-tab--on{% endif %}"
|
||||||
<div class="pool-col__header">
|
data-star="{{ star }}" onclick="setActiveStar({{ star }})">
|
||||||
<span class="star-chip star-chip--{{ star }}">{{ star }}★</span>
|
<span class="star-tab__label">{{ star }}★</span>
|
||||||
</div>
|
<span class="star-tab__count" id="tab-count-{{ star }}">0</span>
|
||||||
<div class="pool-items" id="pool-items-{{ star }}"></div>
|
<label class="star-tab__toggle" onclick="event.stopPropagation()" title="Включить/выключить">
|
||||||
<textarea name="pool_{{ star }}_raw" id="pool-{{ star }}-hidden" hidden>{{ reply_pools[star] }}</textarea>
|
<input type="checkbox" name="stars" value="{{ star }}"
|
||||||
<button type="button" class="btn-add-item" onclick="addPoolItem('{{ star }}')">+ Добавить ответ</button>
|
id="star-cb-{{ star }}"
|
||||||
|
{% if star in enabled_stars %}checked{% endif %}
|
||||||
|
onchange="onStarToggle({{ star }})">
|
||||||
|
<span class="star-tab__toggle-track"></span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div style="margin-top:20px">
|
<div class="settings-footer">
|
||||||
<button type="submit">Сохранить настройки</button>
|
<button type="submit" class="btn-save">Сохранить настройки</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
@@ -180,53 +193,43 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Журнал автоответов</h2>
|
<h2>Журнал автоответов</h2>
|
||||||
<span class="badge">последние 100</span>
|
{% if auto_reply_logs %}
|
||||||
|
<span class="badge">{{ auto_reply_logs|length }}</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if auto_reply_logs %}
|
{% if auto_reply_logs %}
|
||||||
<div class="logs-table-wrap">
|
<div class="log-list">
|
||||||
<table class="logs-table">
|
{% for log in auto_reply_logs %}
|
||||||
<thead>
|
<div class="log-entry log-entry--{{ log['status'] }}">
|
||||||
<tr>
|
<div class="log-entry__left">
|
||||||
<th>Дата лога</th>
|
<span class="log-entry__rating rating--{{ log['rating'] }}">{{ log['rating'] }}★</span>
|
||||||
<th>Дата оценки</th>
|
<div class="log-entry__status">
|
||||||
<th>Оценка</th>
|
{% if log['status'] == 'sent' %}
|
||||||
<th>Товар</th>
|
<span class="log-status--sent">✓ Отправлен</span>
|
||||||
<th>Покупатель</th>
|
{% elif log['status'] == 'skipped' %}
|
||||||
<th>Текст отзыва</th>
|
<span class="log-status--skip">— Пропущен</span>
|
||||||
<th>Статус</th>
|
{% else %}
|
||||||
<th>ID отзыва</th>
|
<span class="log-status--error">✗ Ошибка</span>
|
||||||
<th>Ответ</th>
|
{% endif %}
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
<div class="log-entry__body">
|
||||||
{% for log in auto_reply_logs %}
|
<div class="log-entry__meta">
|
||||||
<tr>
|
<span class="log-entry__product">{{ log['product_name'] or '—' }}{% if log['nm_id'] %} <span class="tag-article">#{{ log['nm_id'] }}</span>{% endif %}</span>
|
||||||
<td>{{ log["created_at"]|format_log_datetime }}</td>
|
<span class="log-entry__buyer">{{ log['user_name'] or '—' }}</span>
|
||||||
<td>{{ log["review_created_at"]|format_log_datetime if log["review_created_at"] else "—" }}</td>
|
<span class="log-entry__time">{{ log['created_at']|format_log_datetime }}</span>
|
||||||
<td><span class="rating rating--{{ log['rating'] }}">{{ log["rating"] }}★</span></td>
|
</div>
|
||||||
<td>
|
{% if log['review_text'] %}
|
||||||
{{ log["product_name"] or "—" }}
|
<div class="log-entry__review">{{ log['review_text'] }}</div>
|
||||||
{% if log["nm_id"] %}
|
{% endif %}
|
||||||
<span class="tag-article">#{{ log["nm_id"] }}</span>
|
{% if log['reply_text'] %}
|
||||||
{% endif %}
|
<div class="log-entry__reply">
|
||||||
</td>
|
<span class="log-entry__reply-ico">↩</span>{{ log['reply_text'] }}
|
||||||
<td>{{ log["user_name"] or "—" }}</td>
|
</div>
|
||||||
<td>{{ log["review_text"] or "—" }}</td>
|
{% endif %}
|
||||||
<td>
|
</div>
|
||||||
{% if log["status"] == "sent" %}
|
</div>
|
||||||
<span class="log-status--sent">✓ Отправлен</span>
|
{% endfor %}
|
||||||
{% 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>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">Пока нет записей автоответа.</div>
|
<div class="empty-state">Пока нет записей автоответа.</div>
|
||||||
@@ -281,51 +284,97 @@ function startCountdown(id, onZero) {
|
|||||||
const cooldownCtrl = startCountdown('cooldown-counter');
|
const cooldownCtrl = startCountdown('cooldown-counter');
|
||||||
const fetchCtrl = startCountdown('fetch-counter', () => window.location.reload());
|
const fetchCtrl = startCountdown('fetch-counter', () => window.location.reload());
|
||||||
|
|
||||||
// ── Pool editor ────────────────────────────────────────────────────
|
// ── Star tab pool editor ───────────────────────────────────────────
|
||||||
function syncHidden(star) {
|
const poolData = {1:[],2:[],3:[],4:[],5:[]};
|
||||||
const items = document.querySelectorAll(`#pool-items-${star} .pool-item-input`);
|
let activeStar = 5;
|
||||||
const lines = [...items].map(i => i.value.trim()).filter(Boolean);
|
|
||||||
const container = document.getElementById(`pool-items-${star}`);
|
function esc(s) {
|
||||||
container.querySelectorAll('input[name="pool_' + star + '_item"]').forEach(e => e.remove());
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
lines.forEach(line => {
|
}
|
||||||
const inp = document.createElement('input');
|
|
||||||
inp.type = 'hidden';
|
function updateTabCount(star) {
|
||||||
inp.name = `pool_${star}_item`;
|
const el = document.getElementById(`tab-count-${star}`);
|
||||||
inp.value = line;
|
if (el) el.textContent = poolData[star].length;
|
||||||
container.appendChild(inp);
|
}
|
||||||
|
|
||||||
|
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 = '') {
|
function renderPanelHeader() {
|
||||||
const container = document.getElementById(`pool-items-${star}`);
|
const LABELS = {1:'1★ — Плохо',2:'2★ — Не понравилось',3:'3★ — Нормально',4:'4★ — Хорошо',5:'5★ — Отлично'};
|
||||||
const row = document.createElement('div');
|
const COLORS = {1:'#EF4444',2:'#F97316',3:'#EAB308',4:'#3B82F6',5:'#22C55E'};
|
||||||
row.className = 'pool-item';
|
const header = document.getElementById('star-panel-header');
|
||||||
row.innerHTML = `<input type="text" class="pool-item-input" value="${value.replace(/"/g,'"')}" placeholder="Текст ответа…"><button type="button" class="btn-delete-item" title="Удалить">✕</button>`;
|
if (header) {
|
||||||
row.querySelector('.pool-item-input').addEventListener('input', () => syncHidden(star));
|
header.innerHTML = `<span style="color:${COLORS[activeStar]};font-weight:700;font-size:15px">${LABELS[activeStar]}</span>
|
||||||
row.querySelector('.btn-delete-item').addEventListener('click', () => { row.remove(); syncHidden(star); });
|
<span style="color:var(--c-text-muted);font-size:13px">${poolData[activeStar].length ? poolData[activeStar].length + ' шаблон(а/ов)' : 'нет шаблонов'}</span>`;
|
||||||
container.appendChild(row);
|
}
|
||||||
syncHidden(star);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 => {
|
[1,2,3,4,5].forEach(star => {
|
||||||
const cb = document.querySelector(`input[name="stars"][value="${star}"]`);
|
const hidden = document.getElementById(`pool-${star}-hidden`);
|
||||||
const col = document.getElementById(`pool-col-${star}`);
|
if (hidden) hidden.value = poolData[star].filter(s => s.trim()).join('\n');
|
||||||
if (col) col.style.display = cb && cb.checked ? '' : 'none';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPool(star) {
|
function initPools() {
|
||||||
const hidden = document.getElementById(`pool-${star}-hidden`);
|
[1,2,3,4,5].forEach(star => {
|
||||||
if (!hidden) return;
|
const hidden = document.getElementById(`pool-${star}-hidden`);
|
||||||
hidden.value.split('\n').map(l => l.trim()).filter(Boolean).forEach(line => addPoolItem(star, line));
|
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', () => {
|
document.getElementById('settings-form').addEventListener('submit', serializeForSubmit);
|
||||||
[1,2,3,4,5].forEach(syncHidden);
|
document.getElementById('btn-add-template')?.addEventListener('click', addTemplate);
|
||||||
});
|
initPools();
|
||||||
|
|
||||||
[1,2,3,4,5].forEach(initPool);
|
|
||||||
|
|
||||||
// ── API polling ────────────────────────────────────────────────────
|
// ── API polling ────────────────────────────────────────────────────
|
||||||
(() => {
|
(() => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- Левая панель — маркетинг -->
|
<!-- Левая панель — маркетинг -->
|
||||||
<div class="login-promo">
|
<div class="login-promo">
|
||||||
<div class="login-promo__inner">
|
<div class="login-promo__inner">
|
||||||
<img src="{{ url_for('static', filename='wb2.png') }}" class="login-promo__logo" alt="WB Feedback">
|
<img src="{{ url_for('static', filename='wb3.png') }}" class="login-promo__logo" alt="WB Feedback">
|
||||||
|
|
||||||
<h1 class="login-promo__title">Автоответы на отзывы<br>Wildberries — на автопилоте</h1>
|
<h1 class="login-promo__title">Автоответы на отзывы<br>Wildberries — на автопилоте</h1>
|
||||||
<p class="login-promo__sub">Сервис сам отвечает на отзывы покупателей пока вы занимаетесь бизнесом. Никаких ручных ответов, никаких пропущенных оценок.</p>
|
<p class="login-promo__sub">Сервис сам отвечает на отзывы покупателей пока вы занимаетесь бизнесом. Никаких ручных ответов, никаких пропущенных оценок.</p>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<div class="login-form-wrap">
|
<div class="login-form-wrap">
|
||||||
<section class="auth-card">
|
<section class="auth-card">
|
||||||
<div class="auth-kicker">
|
<div class="auth-kicker">
|
||||||
<img src="{{ url_for('static', filename='wb2.png') }}" class="auth-kicker-logo-img" alt="WB">
|
<img src="{{ url_for('static', filename='wb3.png') }}" class="auth-kicker-logo-img" alt="WB">
|
||||||
<span class="auth-kicker-text">WB Feedback</span>
|
<span class="auth-kicker-text">WB Feedback</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="login-form-title">Войдите в кабинет</h2>
|
<h2 class="login-form-title">Войдите в кабинет</h2>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="auth-shell">
|
<div class="auth-shell">
|
||||||
<section class="auth-card">
|
<section class="auth-card">
|
||||||
<div class="auth-kicker">
|
<div class="auth-kicker">
|
||||||
<img src="{{ url_for('static', filename='wb2.png') }}" class="auth-kicker-logo-img" alt="WB">
|
<img src="{{ url_for('static', filename='wb3.png') }}" class="auth-kicker-logo-img" alt="WB">
|
||||||
<span class="auth-kicker-text">Wildberries Feedback</span>
|
<span class="auth-kicker-text">Wildberries Feedback</span>
|
||||||
</div>
|
</div>
|
||||||
<h1>Запрос доступа</h1>
|
<h1>Запрос доступа</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user