@@ -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-option s" >
< label class = "filter-option " >
< div class = "filter-pill s" >
< 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 ( ) ) ;
// ── P ool 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 p ool 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 ────────────────────────────────────────────────────
( ( ) => {