feat: implement backend API clients for multi-marketplace integration and frontend service layer
This commit is contained in:
@@ -52,3 +52,18 @@ export async function listFiles(caseId) {
|
||||
const res = await fetch(`${API_BASE}/cases/${caseId}/files`);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function getConfiguredPlatforms() {
|
||||
const res = await fetch(`${API_BASE}/credentials`);
|
||||
const data = await res.json();
|
||||
return data.configured_platforms || [];
|
||||
}
|
||||
|
||||
export async function saveCredentials(platform, data) {
|
||||
const res = await fetch(`${API_BASE}/credentials/${platform}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
215
frontend/js/app.js
Normal file
215
frontend/js/app.js
Normal file
@@ -0,0 +1,215 @@
|
||||
// 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' }
|
||||
];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initTabs();
|
||||
initCases();
|
||||
initIntegrations();
|
||||
});
|
||||
|
||||
function initTabs() {
|
||||
const btns = document.querySelectorAll('.tab-btn');
|
||||
const sections = ['cases-section', 'integrations-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 => {
|
||||
document.getElementById(s).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 === '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);
|
||||
await api.saveCredentials(p.id, data);
|
||||
alert(`Настройки для ${p.name} сохранены!`);
|
||||
loadIntegrations();
|
||||
});
|
||||
|
||||
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('Ошибка при сохранении');
|
||||
}
|
||||
});
|
||||
|
||||
loadCases();
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user