Files
AutoMarket/frontend/js/app.js
2026-05-16 03:05:59 +03:00

1002 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// frontend/js/app.js
import * as api from './api.js';
const PLATFORMS = [
{ id: 'ozon', name: 'Ozon' },
{ id: 'wb', name: 'Wildberries' },
{ id: 'ym', name: 'Yandex Market' },
{ id: 'mm', name: 'Magnit Market' },
{ id: 'mega', name: 'MegaMarket' },
{ id: 'ae', name: 'AliExpress' },
{ id: 'avito', name: 'Avito' },
{ id: 'lamoda', name: 'Lamoda' },
{ id: 'lemana', name: 'Lemana PRO' },
{ id: 'flowwow', name: 'Flowwow' }
];
let currentUser = null;
let socket = null;
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let icon = 'info';
if(type === 'success') icon = 'check-circle';
if(type === 'error') icon = 'alert-circle';
toast.innerHTML = `
<i data-lucide="${icon}" class="icon-sm"></i>
<div class="toast-message">${message}</div>
`;
container.appendChild(toast);
if (window.lucide) lucide.createIcons();
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 4000);
}
function initSocket() {
// Подключаемся напрямую к бэкенду на порту 5000
try { socket = io(`http://${window.location.hostname}:5000`); } catch(e) { console.warn('Socket.IO:', e.message); return; }
socket.on('connect', () => {
console.log('Connected to WebSocket server');
});
socket.on('case_created', (data) => {
showToast(data.message, 'success');
// Refresh dashboard if active
if(!document.getElementById('dashboard-section').classList.contains('hidden')) {
loadDashboard();
}
});
socket.on('scanner_update', (data) => {
showToast(data.message, 'info');
});
}
document.addEventListener("DOMContentLoaded", async () => {
initAuth();
initReports();
try { initSocket(); } catch(e) { console.warn('Socket.IO не подключен:', e.message); }
// Check if logged in
const token = localStorage.getItem('token');
if (token) {
try {
currentUser = await api.getMe();
const planBadge = currentUser.plan === 'pro' ? '<span style="color: gold;">[PRO]</span>' : '<span style="color: gray;">[FREE]</span>';
document.getElementById('username-display').innerHTML = `Привет, ${currentUser.username} ${planBadge}`;
showApp();
if (window.lucide) lucide.createIcons();
} catch (e) {
showAuth();
}
} else {
showAuth();
}
});
let isLoginMode = true;
function initAuth() {
const authForm = document.getElementById('auth-form');
const toggleBtn = document.getElementById('auth-toggle');
const title = document.getElementById('auth-title');
const submitBtn = document.getElementById('auth-submit-btn');
const logoutBtn = document.getElementById('logout-btn');
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
isLoginMode = !isLoginMode;
title.textContent = isLoginMode ? 'Вход в систему' : 'Регистрация';
toggleBtn.textContent = isLoginMode ? 'Нет аккаунта? Зарегистрироваться' : 'Уже есть аккаунт? Войти';
submitBtn.textContent = isLoginMode ? 'Войти' : 'Зарегистрироваться';
});
authForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('auth-username').value;
const password = document.getElementById('auth-password').value;
try {
if (isLoginMode) {
const res = await api.login(username, password);
localStorage.setItem('token', res.token);
// Сразу показываем приложение без перезагрузки
currentUser = await api.getMe();
const planBadge = currentUser.plan === 'pro' ? '<span style="color: gold;">[PRO]</span>' : '<span style="color: gray;">[FREE]</span>';
document.getElementById('username-display').innerHTML = `Привет, ${currentUser.username} ${planBadge}`;
showApp();
if (window.lucide) lucide.createIcons();
} else {
await api.register(username, password);
isLoginMode = true;
title.textContent = 'Вход в систему';
toggleBtn.textContent = 'Нет аккаунта? Зарегистрироваться';
submitBtn.textContent = 'Войти';
showToast('Регистрация успешна! Войдите в систему.', 'success');
}
} catch (err) {
alert(err.message);
}
});
logoutBtn.addEventListener('click', () => {
localStorage.removeItem('token');
window.location.reload();
});
}
function showAuth() {
document.getElementById('auth-section').classList.remove('hidden');
document.getElementById('main-app').classList.add('hidden');
document.getElementById('user-info').classList.add('hidden');
}
function showApp() {
document.getElementById('auth-section').classList.add('hidden');
document.getElementById('main-app').classList.remove('hidden');
document.getElementById('user-info').classList.remove('hidden');
try { initTabs(); } catch(e) { console.error('initTabs:', e); }
try { initCases(); } catch(e) { console.error('initCases:', e); }
try { loadIntegrations(); } catch(e) { console.error('loadIntegrations:', e); }
try { initBilling(); } catch(e) { console.error('initBilling:', e); }
try { initDashboard(); } catch(e) { console.error('initDashboard:', e); }
}
function initDashboard() {
loadDashboard();
}
let casesChart = null;
async function loadDashboard() {
try {
const data = await api.getDashboardStats();
// Update Counters
document.getElementById('stat-cases').innerText = data.total_cases;
document.getElementById('stat-integrations').innerText = data.total_integrations;
document.getElementById('stat-sales').innerText = `${(data.total_sales || 0).toLocaleString()}`;
// Update Stores Table (Multi-account)
const storesTbody = document.querySelector('#stores-table tbody');
if (storesTbody) {
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);
});
}
// Render Chart
const ctx = document.getElementById('casesChart').getContext('2d');
if (casesChart) casesChart.destroy();
casesChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend.map(t => t.date),
datasets: [{
label: 'Новые кейсы',
data: data.trend.map(t => t.count),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true,
borderWidth: 3,
pointRadius: 4,
pointBackgroundColor: '#3b82f6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, border: { display: false } },
x: { grid: { display: false }, border: { display: false } }
}
}
});
// Update Recent Activity Table
const tbody = document.querySelector('#recent-activity-table tbody');
if (tbody) {
tbody.innerHTML = '';
data.recent_activity.forEach(a => {
const tr = document.createElement('tr');
const pName = PLATFORMS.find(p => p.id === a.platform)?.name || a.platform;
const statusClass = a.status === 'new' ? 'status-new' : (a.status === 'closed' ? 'status-closed' : '');
tr.innerHTML = `
<td>#${a.id}</td>
<td><strong>${pName}</strong></td>
<td><code>${a.article}</code></td>
<td><span class="status-pill ${statusClass}">${a.status}</span></td>
<td>${new Date(a.date).toLocaleDateString()}</td>
`;
tbody.appendChild(tr);
});
}
if (window.lucide) lucide.createIcons();
} catch (e) {
console.error('Dashboard Load Error:', e);
}
}
function initBilling() {
document.getElementById('current-plan-display').textContent = currentUser.plan.toUpperCase();
document.getElementById('upgrade-btn').addEventListener('click', async () => {
if(currentUser.plan === 'pro') {
alert('У вас уже тариф PRO!');
return;
}
if(confirm('Перейти на PRO тариф (симуляция оплаты)?')) {
try {
await api.upgradePlan();
alert('Оплата прошла успешно! Вы теперь PRO.');
window.location.reload();
} catch (err) {
alert(err.message);
}
}
});
}
function initTabs() {
const btns = document.querySelectorAll('.tab-btn');
const sections = ['dashboard-section', 'cases-section', 'integrations-section', 'billing-section'];
btns.forEach(btn => {
btn.addEventListener('click', () => {
btns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const target = btn.dataset.target;
sections.forEach(s => {
const el = document.getElementById(s);
if (el) el.classList.add('hidden');
});
document.getElementById(target).classList.remove('hidden');
document.getElementById('case-form-section').classList.add('hidden');
document.getElementById('case-detail-section').classList.add('hidden');
if(target === 'dashboard-section') loadDashboard();
if(target === 'cases-section') loadCases();
if(target === 'integrations-section') loadIntegrations();
if(target === 'logs-section') loadLogs();
if(target === 'reviews-section') { /* No initial load needed */ }
if(target === 'seo-section') loadSEO();
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('/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('/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('/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('/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) {
alert('Введите название товара');
return;
}
const btn = document.getElementById('ai-gen-card-btn');
const originalText = 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('/api/ai/generate-card', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ title })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
document.getElementById('gen-result-container').classList.remove('hidden');
document.getElementById('gen-description').innerText = data.description;
// Tags
const tagsDiv = document.getElementById('gen-tags');
tagsDiv.innerHTML = '';
data.seo_tags.forEach(tag => {
const span = document.createElement('span');
span.className = 'status-pill';
span.style.background = 'rgba(255,255,255,0.1)';
span.innerText = `#${tag}`;
tagsDiv.appendChild(span);
});
// Features
const featuresTbody = document.querySelector('#gen-features-table tbody');
featuresTbody.innerHTML = '';
for (const [key, val] of Object.entries(data.features)) {
const tr = document.createElement('tr');
tr.innerHTML = `<td><strong>${key}</strong></td><td>${val}</td>`;
featuresTbody.appendChild(tr);
}
// Image Prompts
const promptsDiv = document.getElementById('gen-image-prompts');
promptsDiv.innerHTML = '';
data.image_prompts.forEach((p, i) => {
const pBox = document.createElement('div');
pBox.style.background = 'rgba(0,0,0,0.2)';
pBox.style.padding = '10px';
pBox.style.borderRadius = '8px';
pBox.style.marginBottom = '10px';
pBox.style.fontSize = '0.8rem';
pBox.innerHTML = `<strong>Вариант ${i+1}:</strong><br>${p}`;
promptsDiv.appendChild(pBox);
});
showToast('Карточка успешно сгенерирована!', 'success');
} catch (err) {
alert(err.message);
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
if (window.lucide) lucide.createIcons();
}
});
document.getElementById('pnl-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const data = Object.fromEntries(fd.entries());
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/finance/calculate-pnl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
const result = await res.json();
document.getElementById('pnl-results').classList.remove('hidden');
document.getElementById('pnl-net-profit').innerText = `${result.net_profit.toLocaleString()}`;
document.getElementById('pnl-margin').innerText = `${result.margin} %`;
document.getElementById('pnl-roi').innerText = `${result.roi} %`;
document.getElementById('pnl-total-costs').innerText = `${result.total_costs.toLocaleString()}`;
document.getElementById('pnl-tax-abs').innerText = `${result.tax_amount.toLocaleString()}`;
document.getElementById('pnl-comm-abs').innerText = `${result.commission_amount.toLocaleString()}`;
// Color coding
const netProfitEl = document.getElementById('pnl-net-profit');
if (result.net_profit > 0) {
netProfitEl.style.color = 'var(--color-success)';
} else {
netProfitEl.style.color = 'var(--color-error)';
}
showToast('Расчет завершен', 'success');
} catch (err) {
alert('Ошибка расчета');
}
});
async function loadReferences() {
const list = document.getElementById('reference-list');
list.innerHTML = 'Загрузка...';
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/content/reference', {
headers: { 'Authorization': `Bearer ${token}` }
});
const refs = await res.json();
list.innerHTML = '';
refs.forEach(r => {
const div = document.createElement('div');
div.className = 'platform-card';
div.style.textAlign = 'center';
div.innerHTML = `
<div style="background: rgba(0,0,0,0.3); padding: 5px; border-radius: 8px;">#${r.article}</div>
<div style="font-size: 0.6rem; color: var(--color-text-dim); margin: 5px 0;">Hash: ${r.hash.substring(0, 8)}...</div>
<button class="btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.7rem;" onclick="detectClones(${r.id})">Найти клонов</button>
`;
list.appendChild(div);
});
} catch (e) {
list.innerHTML = 'Ошибка';
}
}
window.detectClones = async (id) => {
const container = document.getElementById('clones-detected-container');
const list = document.getElementById('clones-list');
list.innerHTML = 'Ищу визуальные копии на маркетплейсах...';
container.classList.remove('hidden');
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/content/detect-clones/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const clones = await res.json();
list.innerHTML = '';
clones.forEach(c => {
const div = document.createElement('div');
div.className = 'platform-card';
div.style.borderColor = 'var(--color-error)';
div.innerHTML = `
<img src="${c.image_url}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 0.5rem;">
<strong>${c.platform.toUpperCase()}</strong>
<div style="font-size: 0.8rem;">Артикул: ${c.article}</div>
<div style="color: var(--color-error); font-weight: bold;">Сходство: ${c.similarity}</div>
<button class="btn-primary" style="margin-top: 0.5rem; width: 100%;" onclick="reportClone('${c.platform}', '${c.url}', '${c.article}')">Подать жалобу</button>
`;
list.appendChild(div);
});
} catch (e) {
list.innerHTML = 'Ошибка при поиске';
}
}
window.reportClone = (platform, url, article) => {
// Switch to cases tab and pre-fill form
document.querySelector('.tab-btn[data-target="cases-section"]').click();
setTimeout(() => {
document.getElementById('case-platform').value = platform;
document.querySelector('input[name="violator_url"]').value = url;
document.querySelector('input[name="violator_article"]').value = article;
showToast('Данные нарушителя перенесены в форму жалобы', 'info');
}, 100);
}
document.getElementById('add-reference-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const btn = e.target.querySelector('button');
btn.disabled = true;
btn.innerText = 'Загрузка...';
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/content/reference', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
if (res.ok) {
showToast('Эталон загружен и хеширован', 'success');
e.target.reset();
loadReferences();
}
} catch (err) {
alert('Ошибка');
} finally {
btn.disabled = false;
btn.innerText = 'Сохранить эталон';
}
});
let seoChart = null;
async function loadSEO() {
const tbody = document.querySelector('#seo-table tbody');
tbody.innerHTML = '<tr><td colspan="4">Загрузка...</td></tr>';
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/seo/keywords', {
headers: { 'Authorization': `Bearer ${token}` }
});
const keywords = await res.json();
tbody.innerHTML = '';
keywords.forEach(k => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><strong>${k.keyword}</strong></td>
<td><code>${k.article}</code></td>
<td><span class="status-pill status-closed" style="background: rgba(59, 130, 246, 0.1); color: #60a5fa;">#${k.last_position || '?'}</span></td>
<td>
<button class="btn-secondary" onclick="viewSEOHistory(${k.id}, '${k.keyword}')">График</button>
</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
tbody.innerHTML = '<tr><td colspan="4">Ошибка загрузки</td></tr>';
}
}
window.viewSEOHistory = async (id, keyword) => {
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/seo/history/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const history = await res.json();
document.getElementById('seo-chart-container').classList.remove('hidden');
document.getElementById('seo-chart-title').innerText = `История позиции: ${keyword}`;
const ctx = document.getElementById('seoChart').getContext('2d');
if (seoChart) seoChart.destroy();
seoChart = new Chart(ctx, {
type: 'line',
data: {
labels: history.map(h => h.date),
datasets: [{
label: 'Место в поиске',
data: history.map(h => h.position),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.3,
fill: true,
reverse: true // Because 1 is better than 100
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { reverse: true, grid: { color: 'rgba(255,255,255,0.05)' } },
x: { grid: { display: false } }
}
}
});
} catch (e) {
alert('Ошибка загрузки истории');
}
}
document.getElementById('add-seo-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const data = Object.fromEntries(fd.entries());
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/seo/keywords', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
if (res.ok) {
e.target.reset();
loadSEO();
showToast('Ключевое слово добавлено', 'success');
}
} catch (err) {
alert('Ошибка при добавлении');
}
});
async function loadLogs() {
const tbody = document.querySelector('#logs-table tbody');
tbody.innerHTML = '<tr><td colspan="3">Загрузка...</td></tr>';
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/dashboard/logs', {
headers: { 'Authorization': `Bearer ${token}` }
});
const logs = await res.json();
tbody.innerHTML = '';
logs.forEach(l => {
const tr = document.createElement('tr');
const details = l.details ? JSON.stringify(l.details) : '-';
tr.innerHTML = `
<td><strong>${l.action}</strong></td>
<td style="font-size: 0.8rem; color: var(--color-text-dim);">${details}</td>
<td>${new Date(l.date).toLocaleString()}</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
tbody.innerHTML = '<tr><td colspan="3">Ошибка загрузки</td></tr>';
}
}
// ---- INTEGRATIONS ----
async function loadIntegrations() {
const grid = document.getElementById('platforms-grid');
grid.innerHTML = 'Загрузка...';
try {
const configured = await api.getConfiguredPlatforms();
grid.innerHTML = '';
PLATFORMS.forEach(p => {
const isConfigured = configured.includes(p.id);
const card = document.createElement('div');
card.className = `platform-card ${isConfigured ? 'active-platform' : ''}`;
card.innerHTML = `
<h3>${p.name}</h3>
<p class="status">${isConfigured ? '✅ Подключено' : '❌ Не настроено'}</p>
<form class="platform-form" data-id="${p.id}">
<input type="text" name="api_key" placeholder="API Key / Token" />
<input type="text" name="client_id" placeholder="Client ID (если есть)" />
<input type="text" name="client_secret" placeholder="Client Secret (если есть)" />
<button type="submit" class="btn-primary">Сохранить</button>
</form>
`;
card.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
try {
await api.saveCredentials(p.id, data);
alert(`Настройки для ${p.name} сохранены!`);
loadIntegrations();
} catch(err) {
alert(err.message);
}
});
grid.appendChild(card);
});
if (window.lucide) lucide.createIcons();
} catch (e) {
grid.innerHTML = 'Ошибка загрузки интеграций.';
console.error(e);
}
}
// ---- CASES ----
async function initCases() {
document.getElementById('new-case-btn').addEventListener('click', () => {
document.getElementById('cases-section').classList.add('hidden');
document.getElementById('case-form-section').classList.remove('hidden');
document.getElementById('case-form').reset();
});
document.getElementById('cancel-btn').addEventListener('click', () => {
document.getElementById('case-form-section').classList.add('hidden');
document.getElementById('cases-section').classList.remove('hidden');
});
document.getElementById('back-to-list').addEventListener('click', () => {
document.getElementById('case-detail-section').classList.add('hidden');
document.getElementById('cases-section').classList.remove('hidden');
loadCases();
});
document.getElementById('search-input').addEventListener('input', debounce(loadCases, 500));
document.getElementById('status-filter').addEventListener('change', loadCases);
document.getElementById('platform-filter').addEventListener('change', loadCases);
document.getElementById('case-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
try {
const res = await api.createCase(data);
const fileInput = document.getElementById('file-input');
if (fileInput.files.length > 0) {
await api.uploadFiles(res.id, fileInput.files);
}
alert('Кейс успешно создан!');
document.getElementById('case-form-section').classList.add('hidden');
document.getElementById('cases-section').classList.remove('hidden');
loadCases();
} catch (err) {
console.error(err);
alert('Ошибка при сохранении');
}
});
document.getElementById('export-csv-btn').addEventListener('click', () => {
window.location.href = api.getExportCsvUrl();
});
document.getElementById('ai-gen-btn').addEventListener('click', async () => {
const platform = document.getElementById('case-platform').value;
const v_art = document.querySelector('input[name="violator_article"]').value;
const my_art = document.querySelector('input[name="my_article"]').value;
if (!v_art) {
alert('Сначала введите артикул нарушителя');
return;
}
const btn = document.getElementById('ai-gen-btn');
const originalText = 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('/api/ai/generate-complaint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ platform, violator_article: v_art, my_article: my_art })
});
const data = await res.json();
if (data.text) {
document.getElementById('case-comment').value = data.text;
showToast('Текст успешно сгенерирован ИИ', 'success');
} else {
alert(data.error || 'Ошибка генерации');
}
} catch (err) {
alert('Ошибка сети при вызове ИИ');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
if (window.lucide) lucide.createIcons();
}
});
// ---- REVIEWS AI ----
document.getElementById('ai-review-btn').addEventListener('click', async () => {
const text = document.getElementById('review-text-input').value;
const rating = document.getElementById('review-rating-input').value;
if (!text) {
alert('Введите текст отзыва');
return;
}
const btn = document.getElementById('ai-review-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('/api/ai/generate-reply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ review_text: text, rating: parseInt(rating) })
});
const data = await res.json();
if (data.text) {
document.getElementById('ai-reply-output').value = data.text;
document.getElementById('ai-reply-container').classList.remove('hidden');
showToast('Ответ сгенерирован!', 'success');
} else {
alert(data.error || 'Ошибка');
}
} catch (err) {
alert('Ошибка сети');
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
});
window.copyReply = () => {
const output = document.getElementById('ai-reply-output');
output.select();
document.execCommand('copy');
showToast('Скопировано в буфер!', 'info');
};
loadCases();
}
let currentViewCaseId = null;
async function initReports() {
document.getElementById('download-pdf-btn').addEventListener('click', () => {
if (currentViewCaseId) {
window.location.href = api.getCaseReportPdfUrl(currentViewCaseId);
}
});
}
// Дублирующий loadDashboard удалён — используется функция из строки ~149
window.viewCase = async function(id) {
currentViewCaseId = id;
document.getElementById('cases-section').classList.add('hidden');
const detailSection = document.getElementById('case-detail-section');
detailSection.classList.remove('hidden');
const caseObj = await api.getCase(id);
const files = await api.listFiles(id);
let html = `
<p><strong>Площадка:</strong> ${PLATFORMS.find(p => p.id === caseObj.platform)?.name || caseObj.platform}</p>
<p><strong>Артикул нарушителя:</strong> ${caseObj.violator_article}</p>
<p><strong>Ссылка:</strong> <a href="${caseObj.violator_url}" target="_blank">${caseObj.violator_url}</a></p>
<p><strong>Ваш артикул:</strong> ${caseObj.my_article || '-'}</p>
<p><strong>Комментарий:</strong> ${caseObj.comment || '-'}</p>
<p><strong>Статус:</strong> ${caseObj.status}</p>
`;
if (files && files.length > 0) {
html += `<h3>Файлы доказательств:</h3><div class="preview">`;
files.forEach(f => {
const url = `/api/uploads/${f.path.split('/').pop()}`;
html += `<a href="${url}" target="_blank"><img src="${url}" alt="Док" /></a>`;
});
html += `</div>`;
}
document.getElementById('case-detail').innerHTML = html;
};
window.deleteCase = async function(id) {
if(confirm('Удалить этот кейс?')) {
await api.deleteCase(id);
loadCases();
}
};
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}