feat: implement backend core with repricing logic, case management, and multi-service architecture

This commit is contained in:
2026-05-13 00:28:13 +03:00
parent 38f3199c33
commit d9aae5b85e
15 changed files with 596 additions and 90 deletions

View File

@@ -51,6 +51,8 @@
<button class="tab-btn" data-target="content-section"><i data-lucide="image" class="icon-sm"></i> Контент</button>
<button class="tab-btn" data-target="finance-section"><i data-lucide="wallet" class="icon-sm"></i> Финансы</button>
<button class="tab-btn" data-target="generator-section"><i data-lucide="wand-2" class="icon-sm"></i> Генератор</button>
<button class="tab-btn" data-target="analytics-section"><i data-lucide="search-code" class="icon-sm"></i> Deep Insight</button>
<button class="tab-btn" data-target="repricer-section"><i data-lucide="tags" class="icon-sm"></i> Репрайзер</button>
</div>
<main class="container">
@@ -85,6 +87,8 @@
<th>Магазин</th>
<th>Площадка</th>
<th>Статус API</th>
<th>Остатки (шт)</th>
<th>Дней до OOS</th>
<th>Продажи (сегодня)</th>
</tr>
</thead>
@@ -355,6 +359,94 @@
</div>
</section>
<!-- Deep Insight Section -->
<section id="analytics-section" class="section hidden">
<h2>AI Анализ конкурентов (Deep Insight)</h2>
<div class="platforms-grid">
<div class="platform-card">
<h3>Сравнение товаров</h3>
<p style="color: var(--color-text-dim); font-size: 0.9rem; margin-bottom: 1rem;">
Вставьте описание вашего товара и описание конкурента, чтобы ИИ нашел способ его победить.
</p>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<label>Мое описание<textarea id="my-desc-input" rows="4" placeholder="Ваше описание..."></textarea></label>
<label>Описание конкурента<textarea id="comp-desc-input" rows="4" placeholder="Описание конкурента..."></textarea></label>
<button class="btn-primary" id="ai-analyze-comp-btn">
<i data-lucide="zap" class="icon-sm"></i> Запустить Deep Insight
</button>
</div>
</div>
<div id="insight-result" class="platform-card hidden" style="grid-column: 2 / -1;">
<div style="background: rgba(var(--color-primary-rgb), 0.1); border-left: 4px solid var(--color-primary); padding: 1rem; margin-bottom: 1.5rem;">
<h4 style="color: var(--color-primary);">Мощный слоган (Win Phrase):</h4>
<p id="insight-win-phrase" style="font-style: italic; font-size: 1.1rem; margin-top: 0.5rem;"></p>
</div>
<div class="platforms-grid" style="grid-template-columns: 1fr 1fr; gap: 1.5rem;">
<div>
<h4>Слабые места конкурента</h4>
<ul id="insight-weakness" style="margin-top: 0.5rem; color: var(--color-error); font-size: 0.9rem;"></ul>
</div>
<div>
<h4>Твои преимущества</h4>
<ul id="insight-advantages" style="margin-top: 0.5rem; color: var(--color-success); font-size: 0.9rem;"></ul>
</div>
</div>
<h4 style="margin-top: 1.5rem;">Стратегия захвата рынка</h4>
<p id="insight-strategy" style="margin-top: 0.5rem; line-height: 1.6; color: var(--color-text-dim);"></p>
</div>
</div>
</section>
<!-- Repricer Section -->
<section id="repricer-section" class="section hidden">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h2>Авто-репрайзер (Динамические цены)</h2>
<button class="btn-primary" id="run-repricer-btn">
<i data-lucide="refresh-cw" class="icon-sm"></i> Обновить цены сейчас
</button>
</div>
<div class="platforms-grid">
<div class="platform-card" style="grid-column: 1 / 1;">
<h3>Новое правило</h3>
<form id="repricer-form" class="platform-form" style="margin-top: 1rem; display: flex; flex-direction: column; gap: 0.8rem;">
<label>Товар<input type="text" name="product_name" placeholder="Название товара" required></label>
<label>SKU / Артикул<input type="text" name="sku" placeholder="Артикул" required></label>
<div style="display: flex; gap: 1rem;">
<label style="flex: 1;">Мин. цена (₽)<input type="number" name="min_price" required></label>
<label style="flex: 1;">Макс. цена (₽)<input type="number" name="max_price" required></label>
</div>
<label>Стратегия
<select name="strategy">
<option value="follow_competitor">На 1₽ дешевле конкурента</option>
<option value="match_lowest">Равняться на минимальную</option>
</select>
</label>
<button type="submit" class="btn-primary">Добавить правило</button>
</form>
</div>
<div class="platform-card" style="grid-column: 2 / -1;">
<h3>Активные правила</h3>
<table id="repricer-table" class="table" style="margin-top: 1rem;">
<thead>
<tr>
<th>Товар</th>
<th>Мин / Макс</th>
<th>Тек. цена</th>
<th>Последнее обновление</th>
<th>Статус</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</section>
<!-- Integrations List -->
<section id="integrations-section" class="section hidden">
<h2>Доступные маркетплейсы</h2>

View File

@@ -161,10 +161,13 @@ async function loadDashboard() {
storesTbody.innerHTML = '';
(data.stores || []).forEach(s => {
const tr = document.createElement('tr');
const oosClass = s.days_left < 7 ? 'color: var(--color-error); font-weight: bold;' : '';
tr.innerHTML = `
<td><strong>${s.name}</strong></td>
<td><span style="text-transform: uppercase;">${s.platform}</span></td>
<td><span class="status-pill status-closed" style="background: rgba(16, 185, 129, 0.1); color: #34d399;">● Online</span></td>
<td>${s.stock}</td>
<td style="${oosClass}">${s.days_left} дн.</td>
<td>${(s.daily_sales || 0).toLocaleString()} ₽</td>
`;
storesTbody.appendChild(tr);
@@ -273,10 +276,146 @@ function initTabs() {
if(target === 'content-section') loadReferences();
if(target === 'finance-section') { /* No initial load */ }
if(target === 'generator-section') { /* No initial load */ }
if(target === 'analytics-section') { /* No initial load */ }
if(target === 'repricer-section') loadRepricerRules();
});
});
}
async function loadRepricerRules() {
const table = document.getElementById('repricer-table').querySelector('tbody');
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:5000/api/repricer/rules', {
headers: { 'Authorization': `Bearer ${token}` }
});
const rules = await res.json();
table.innerHTML = '';
rules.forEach(r => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><strong>${r.product_name}</strong><br><small>${r.sku}</small></td>
<td>${r.min_price} - ${r.max_price} ₽</td>
<td><span style="font-weight: bold; color: var(--color-primary);">${r.current_price ? r.current_price + ' ₽' : '—'}</span></td>
<td>${r.last_run ? new Date(r.last_run).toLocaleString() : 'Никогда'}</td>
<td><span class="status-pill status-${r.is_active ? 'closed' : 'open'}">${r.is_active ? 'Активно' : 'Пауза'}</span></td>
`;
table.appendChild(tr);
});
} catch (err) {
console.error('Error loading repricer rules:', err);
}
}
document.getElementById('repricer-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const data = Object.fromEntries(fd.entries());
data.is_active = true;
try {
const token = localStorage.getItem('token');
await fetch('http://localhost:5000/api/repricer/rules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
showToast('Правило сохранено', 'success');
loadRepricerRules();
e.target.reset();
} catch (err) {
alert('Ошибка сохранения');
}
});
document.getElementById('run-repricer-btn').addEventListener('click', async () => {
const btn = document.getElementById('run-repricer-btn');
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="icon-sm spin"></i> Обновляю...';
if (window.lucide) lucide.createIcons();
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:5000/api/repricer/run', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
showToast(`Обновлено ${data.updates.length} цен`, 'success');
loadRepricerRules();
} catch (err) {
alert('Ошибка при запуске репрайзера');
} finally {
btn.disabled = false;
btn.innerHTML = '<i data-lucide="refresh-cw" class="icon-sm"></i> Обновить цены сейчас';
if (window.lucide) lucide.createIcons();
}
});
document.getElementById('ai-analyze-comp-btn').addEventListener('click', async () => {
const myDesc = document.getElementById('my-desc-input').value;
const compDesc = document.getElementById('comp-desc-input').value;
if (!compDesc) {
alert('Введите описание конкурента');
return;
}
const btn = document.getElementById('ai-analyze-comp-btn');
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="icon-sm spin"></i> Анализирую рынок...';
if (window.lucide) lucide.createIcons();
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:5000/api/ai/analyze-competitor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ my_desc: myDesc, comp_desc: compDesc })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
document.getElementById('insight-result').classList.remove('hidden');
document.getElementById('insight-win-phrase').innerText = data.win_phrase;
document.getElementById('insight-strategy').innerText = data.strategy;
const wList = document.getElementById('insight-weakness');
wList.innerHTML = '';
data.competitor_weakness.forEach(w => {
const li = document.createElement('li');
li.innerText = w;
wList.appendChild(li);
});
const aList = document.getElementById('insight-advantages');
aList.innerHTML = '';
data.my_advantages.forEach(a => {
const li = document.createElement('li');
li.innerText = a;
aList.appendChild(li);
});
showToast('Анализ Deep Insight завершен!', 'success');
} catch (err) {
alert(err.message);
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
});
document.getElementById('ai-gen-card-btn').addEventListener('click', async () => {
const title = document.getElementById('gen-title-input').value;
if (!title) {