// 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 = `
${message}
`; 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' ? '[PRO]' : '[FREE]'; 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' ? '[PRO]' : '[FREE]'; 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 = ` ${s.name} ${s.platform} ● Online ${s.stock} ${s.days_left} дн. ${(s.daily_sales || 0).toLocaleString()} ₽ `; 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 = ` #${a.id} ${pName} ${a.article} ${a.status} ${new Date(a.date).toLocaleDateString()} `; 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', 'logs-section', 'reviews-section', 'seo-section', 'content-section', 'finance-section', 'generator-section', 'analytics-section', 'repricer-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 = ` ${r.product_name}
${r.sku} ${r.min_price} - ${r.max_price} ₽ ${r.current_price ? r.current_price + ' ₽' : '—'} ${r.last_run ? new Date(r.last_run).toLocaleString() : 'Никогда'} ${r.is_active ? 'Активно' : 'Пауза'} `; 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 = ' Обновляю...'; 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 = ' Обновить цены сейчас'; 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 = ' Анализирую рынок...'; 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 = ' Магия ИИ в процессе...'; 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 = `${key}${val}`; 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 = `Вариант ${i+1}:
${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 = `
#${r.article}
Hash: ${r.hash.substring(0, 8)}...
`; 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 = ` ${c.platform.toUpperCase()}
Артикул: ${c.article}
Сходство: ${c.similarity}
`; 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 = 'Загрузка...'; 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 = ` ${k.keyword} ${k.article} #${k.last_position || '?'} `; tbody.appendChild(tr); }); } catch (e) { tbody.innerHTML = 'Ошибка загрузки'; } } 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 = 'Загрузка...'; 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 = ` ${l.action} ${details} ${new Date(l.date).toLocaleString()} `; tbody.appendChild(tr); }); } catch (e) { tbody.innerHTML = 'Ошибка загрузки'; } } // ---- 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 = `

${p.name}

${isConfigured ? '✅ Подключено' : '❌ Не настроено'}

`; 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 = ' Генерирую...'; 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 = ' Генерирую ответ...'; 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 = `

Площадка: ${PLATFORMS.find(p => p.id === caseObj.platform)?.name || caseObj.platform}

Артикул нарушителя: ${caseObj.violator_article}

Ссылка: ${caseObj.violator_url}

Ваш артикул: ${caseObj.my_article || '-'}

Комментарий: ${caseObj.comment || '-'}

Статус: ${caseObj.status}

`; if (files && files.length > 0) { html += `

Файлы доказательств:

`; files.forEach(f => { const url = `/api/uploads/${f.path.split('/').pop()}`; html += `Док`; }); html += `
`; } 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); }; }