377 lines
14 KiB
JavaScript
377 lines
14 KiB
JavaScript
// 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;
|
||
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
initAuth();
|
||
initReports();
|
||
|
||
// 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();
|
||
} 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 logoutBtn = document.getElementById('logout-btn');
|
||
|
||
toggleBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
isLoginMode = !isLoginMode;
|
||
title.textContent = isLoginMode ? 'Вход в систему' : 'Регистрация';
|
||
toggleBtn.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);
|
||
} else {
|
||
await api.register(username, password);
|
||
alert('Регистрация успешна! Теперь вы можете войти.');
|
||
isLoginMode = true;
|
||
title.textContent = 'Вход в систему';
|
||
toggleBtn.textContent = 'Нет аккаунта? Зарегистрироваться';
|
||
return;
|
||
}
|
||
window.location.reload();
|
||
} 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');
|
||
initTabs();
|
||
initCases();
|
||
initIntegrations();
|
||
initBilling();
|
||
initDashboard();
|
||
}
|
||
|
||
function initDashboard() {
|
||
loadDashboard();
|
||
}
|
||
|
||
async function loadDashboard() {
|
||
try {
|
||
const stats = await api.getDashboardStats();
|
||
const grid = document.getElementById('stats-grid');
|
||
grid.innerHTML = `
|
||
<div class="platform-card">
|
||
<h3>Всего кейсов</h3>
|
||
<p style="font-size: 2rem; font-weight: bold;">${stats.total_cases}</p>
|
||
</div>
|
||
<div class="platform-card">
|
||
<h3>Активных интеграций</h3>
|
||
<p style="font-size: 2rem; font-weight: bold;">${stats.total_integrations}</p>
|
||
<p style="font-size: 0.8rem;">${stats.platforms.join(', ').toUpperCase()}</p>
|
||
</div>
|
||
<div class="platform-card">
|
||
<h3>В работе (Prepared/Sent)</h3>
|
||
<p style="font-size: 2rem; font-weight: bold;">
|
||
${(stats.cases_by_status.prepared || 0) + (stats.cases_by_status.sent || 0)}
|
||
</p>
|
||
</div>
|
||
`;
|
||
|
||
const tbody = document.querySelector('#recent-activity-table tbody');
|
||
tbody.innerHTML = '';
|
||
stats.recent_activity.forEach(a => {
|
||
const tr = document.createElement('tr');
|
||
const pName = PLATFORMS.find(p => p.id === a.platform)?.name || a.platform;
|
||
tr.innerHTML = `
|
||
<td>${a.id}</td>
|
||
<td>${pName}</td>
|
||
<td>${a.article}</td>
|
||
<td>${a.status}</td>
|
||
<td>${new Date(a.date).toLocaleDateString()}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
} catch (e) {
|
||
console.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();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ---- 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);
|
||
});
|
||
} 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();
|
||
});
|
||
|
||
loadCases();
|
||
}
|
||
|
||
let currentViewCaseId = null;
|
||
async function initReports() {
|
||
document.getElementById('download-pdf-btn').addEventListener('click', () => {
|
||
if (currentViewCaseId) {
|
||
window.location.href = api.getCaseReportPdfUrl(currentViewCaseId);
|
||
}
|
||
});
|
||
}
|
||
|
||
async function loadCases() {
|
||
const article = document.getElementById('search-input').value;
|
||
const status = document.getElementById('status-filter').value;
|
||
const platform = document.getElementById('platform-filter').value;
|
||
|
||
const tbody = document.querySelector('#cases-table tbody');
|
||
tbody.innerHTML = '<tr><td colspan="6">Загрузка...</td></tr>';
|
||
|
||
try {
|
||
const cases = await api.fetchCases({ article, status, platform });
|
||
tbody.innerHTML = '';
|
||
if (cases.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="6">Нет жалоб</td></tr>';
|
||
return;
|
||
}
|
||
|
||
cases.forEach(c => {
|
||
const tr = document.createElement('tr');
|
||
const pName = PLATFORMS.find(p => p.id === c.platform)?.name || c.platform;
|
||
tr.innerHTML = `
|
||
<td>${c.id}</td>
|
||
<td><strong>${pName}</strong></td>
|
||
<td>${c.violator_article}</td>
|
||
<td>${c.status}</td>
|
||
<td>${new Date(c.created_at).toLocaleString()}</td>
|
||
<td>
|
||
<button class="btn-secondary" onclick="window.viewCase(${c.id})">Просмотр</button>
|
||
<button class="btn-secondary" onclick="window.deleteCase(${c.id})">Удалить</button>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
} catch (err) {
|
||
tbody.innerHTML = '<tr><td colspan="6">Ошибка сети</td></tr>';
|
||
}
|
||
}
|
||
|
||
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 = `http://localhost:5000/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);
|
||
};
|
||
}
|