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

@@ -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) {