feat: implement backend core with repricing logic, case management, and multi-service architecture
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user