ebayDeepScan/public/index.html

730 lines
30 KiB
HTML
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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eBay Scanner Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0b0f19;
--surface: #161b2a;
--surface-light: #1e2539;
--primary: #3b82f6;
--primary-glow: rgba(59, 130, 246, 0.4);
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--text: #f8fafc;
--text-muted: #94a3b8;
--border: #2d3748;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg);
color: var(--text);
line-height: 1.5;
overflow-x: hidden;
}
header {
background: var(--surface);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 100;
}
header h1 {
font-size: 1.3rem;
font-weight: 700;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-tabs {
display: flex; gap: 0.5rem; padding: 1rem 2rem;
background: #0f1422; border-bottom: 1px solid var(--border);
overflow-x: auto;
}
.tab {
padding: 0.6rem 1.2rem; border-radius: 8px; cursor: pointer;
color: var(--text-muted); transition: all 0.2s; white-space: nowrap;
border: 1px solid transparent;
}
.tab:hover { background: var(--surface-light); color: var(--text); }
.tab.active {
background: var(--primary); color: white;
box-shadow: 0 4px 12px var(--primary-glow);
}
.tab-add { border: 1px dashed var(--border); color: var(--primary); }
.action-bar {
padding: 0.75rem 2rem; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 1rem;
background: #0f1422;
}
.status-badge {
display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem;
}
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); }
.status-dot.active {
background: var(--warning);
box-shadow: 0 0 8px var(--warning);
animation: pulse 1.5s infinite;
}
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
button {
padding: 0.5rem 1rem; border: none; border-radius: 6px;
font-weight: 600; cursor: pointer; transition: all 0.2s;
font-family: inherit; font-size: 0.85rem;
}
button:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--primary); color: white; }
.btn-success { background: var(--success); color: white; }
.btn-danger { background: var(--danger); color: white; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { background: var(--border); }
main { padding: 1.5rem 2rem; max-width: 1600px; margin: 0 auto; }
/* Search & Filter */
.toolbar {
display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: center;
}
.toolbar input {
background: var(--surface); border: 1px solid var(--border);
color: var(--text); padding: 0.6rem 1rem; border-radius: 8px;
font-size: 0.9rem; flex: 1; outline: none; transition: border-color 0.2s;
}
.toolbar input:focus { border-color: var(--primary); }
/* Table */
.card { background: var(--surface); border-radius: 12px; border: 1px solid var(--border); overflow: hidden; }
table { width: 100%; border-collapse: collapse; min-width: 1000px; }
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--border); }
th { background: rgba(0,0,0,0.2); font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.05em; }
tr:last-child td { border-bottom: none; }
tr:hover { background: rgba(255,255,255,0.02); }
.td-img { width: 64px; height: 64px; object-fit: cover; border-radius: 6px; cursor: zoom-in; }
.td-title { font-weight: 600; font-size: 0.95rem; line-height: 1.3; color: var(--primary); cursor: pointer; }
.td-title:hover { text-decoration: underline; }
.td-sub { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; }
.price-lg { font-size: 1.1rem; font-weight: 700; color: var(--text); }
.profit-badge { color: var(--success); font-weight: 700; }
/* Modals */
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85); backdrop-filter: blur(8px);
display: flex; justify-content: center; align-items: center;
z-index: 1000; opacity: 0; pointer-events: none; transition: opacity 0.3s;
}
.modal-overlay.active { opacity: 1; pointer-events: all; }
.modal-content {
background: var(--surface); border: 1px solid var(--border);
width: 95vw; max-width: 1400px; max-height: 95vh; border-radius: 16px;
overflow-y: auto; position: relative; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
}
.modal-close {
position: absolute; top: 1.5rem; right: 1.5rem; font-size: 1.5rem;
cursor: pointer; background: none; border: none; color: var(--text-muted);
}
.modal-padd { padding: 2.5rem; }
form div { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.4rem; font-size: 0.9rem; color: var(--text-muted); }
input[type="text"], input[type="number"], textarea {
width: 100%; background: #0f1422; border: 1px solid var(--border);
color: var(--text); padding: 0.75rem; border-radius: 8px; font-family: inherit;
}
.keyword-pill {
display: inline-block; padding: 0.2rem 0.5rem; background: var(--surface-light);
border-radius: 4px; font-size: 0.75rem; margin-right: 4px; margin-bottom: 4px;
border: 1px solid var(--border);
}
/* Zoom */
.zoom-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.95); z-index: 3000;
display: flex; justify-content: center; align-items: center;
opacity: 0; pointer-events: none; transition: 0.2s;
}
.zoom-overlay.active { opacity: 1; pointer-events: all; }
.zoom-overlay img { max-width: 95vw; max-height: 95vh; object-fit: contain; }
.empty-state {
text-align: center; padding: 4rem 2rem; color: var(--text-muted);
}
/* Side actions in modal */
.m-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; }
.m-grid > div { min-width: 0; }
.ai-box {
background: rgba(59, 130, 246, 0.05); border-left: 3px solid var(--primary);
padding: 1rem; border-radius: 4px; font-size: 0.9rem; margin-bottom: 1.5rem;
}
.progress-wrapper {
flex: 1; max-width: 400px; background: var(--surface-light);
height: 8px; border-radius: 4px; overflow: hidden; display: none;
border: 1px solid var(--border);
}
.progress-fill {
height: 100%; background: var(--primary); width: 0%; transition: width 0.3s;
box-shadow: 0 0 10px var(--primary-glow);
}
</style>
</head>
<body>
<header>
<h1>eBay Deep Scan Dashboard</h1>
<div style="display:flex; gap: 0.5rem">
<button class="btn-outline" onclick="triggerScan('all')">🌐 Scan All Profiles</button>
<button class="btn-outline" onclick="openProfileModal()">⚙️ Manage Profiles</button>
<button class="btn-primary" id="btn-scan" onclick="triggerScan()">🚀 Start Scan</button>
</div>
</header>
<div class="nav-tabs" id="profile-tabs">
<!-- Tabs injected here -->
</div>
<div class="action-bar">
<div class="status-badge">
<span class="status-dot" id="status-dot"></span>
<span id="status-text">Ready</span>
</div>
<div class="progress-wrapper" id="progress-wrapper">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div id="progress-info" style="font-size: 0.8rem; color: var(--text-muted); display: none;"></div>
<div style="margin-left: auto; display: flex; gap: 1rem; align-items: center; font-size: 0.85rem">
<span id="last-run-label">Last scan: <b id="last-run-time">-</b></span>
<button class="btn-outline" onclick="openKeywordsModal()" id="btn-manage-kw">📝 Manage Keywords</button>
</div>
</div>
<main>
<div class="toolbar">
<input type="text" id="searchInput" placeholder="Filter by title, part number, seller..." oninput="renderItems()">
<div id="item-count" style="font-size: 0.85rem; color: var(--text-muted)"></div>
</div>
<div class="card">
<table>
<thead>
<tr>
<th width="80">Img</th>
<th>Product Info</th>
<th>Market Price</th>
<th>Profit (Est)</th>
<th>AI Analysis</th>
<th width="150">Review</th>
</tr>
</thead>
<tbody id="item-tbody"></tbody>
</table>
</div>
</main>
<!-- Profile Management Modal -->
<div class="modal-overlay" id="modal-profiles">
<div class="modal-content">
<button class="modal-close" onclick="closeModals()">×</button>
<div class="modal-padd">
<h2 style="margin-bottom: 1.5rem">Manage Scan Profiles</h2>
<div id="profile-list" style="margin-bottom: 2rem"></div>
<h3 style="margin-bottom: 1rem; font-size: 1rem">Create New Profile</h3>
<div class="card" style="padding: 1.5rem">
<div class="m-grid">
<div>
<label>Profile Name</label>
<input type="text" id="new-profile-name" placeholder="e.g. DDR4 RAM, 3060 GPUs">
</div>
<div>
<label>Price Ratio (e.g. 0.85)</label>
<input type="number" id="new-profile-ratio" value="0.85" step="0.01">
</div>
</div>
<button class="btn-primary" style="width: 100%" id="btn-save-profile" onclick="saveProfile()">Add Profile</button>
</div>
</div>
</div>
</div>
<!-- Keywords Management Modal -->
<div class="modal-overlay" id="modal-keywords">
<div class="modal-content" style="max-width: 1100px;">
<button class="modal-close" onclick="closeModals()">×</button>
<div class="modal-padd">
<h2 id="kw-modal-title" style="margin-bottom: 1.5rem">Keywords for ...</h2>
<div class="card" style="padding: 1.5rem; margin-bottom: 2rem; background: var(--surface-light)">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem">
<h3 style="font-size: 0.95rem">Add Search Target</h3>
<button class="btn-outline" onclick="toggleBulkImport()">Toggle Bulk Import</button>
</div>
<div id="single-import-form">
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px">
<label>Part Number (Optional)</label>
<input type="text" id="kw-part" placeholder="e.g. M393A2K43BB1-CTD">
</div>
<div style="flex: 2; min-width: 300px">
<label>Keywords (Comma separated)</label>
<input type="text" id="kw-list" placeholder="e.g. 16GB DDR4 2666 ECC, Samsung 16GB RDIMM">
</div>
<div style="flex: 0.5; min-width: 120px">
<label>Target Price ($)</label>
<input type="number" id="kw-price" placeholder="25.00">
</div>
</div>
<button class="btn-success" style="margin-top: 1rem" id="btn-add-kw" onclick="addKeyword()">Add Target to Profile</button>
</div>
<div id="bulk-import-form" style="display:none">
<label>Paste lines: <code>PartNumber | Keywords | Price</code></label>
<textarea id="bulk-text" rows="8" style="width:100%; background:#0f1422; border:1px solid var(--border); color:white; padding:0.75rem; border-radius:8px; font-family:monospace" placeholder="M393A... | 16GB DDR4..., Samsung... | 25.00"></textarea>
<button class="btn-success" style="margin-top: 1rem; width:100%" onclick="processBulkKeywords()">Import All Lines</button>
</div>
</div>
<div class="card">
<table>
<thead>
<tr><th>Part #</th><th>Search Keywords</th><th>Target $</th><th>Action</th></tr>
</thead>
<tbody id="kw-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Detail Modal -->
<div class="modal-overlay" id="modal-detail">
<div class="modal-content" style="max-width: 1000px;">
<button class="modal-close" onclick="closeModals()">×</button>
<div class="modal-padd" id="detail-body"></div>
</div>
</div>
<!-- Zoom -->
<div class="zoom-overlay" id="zoom-overlay" onclick="this.classList.remove('active')">
<img id="zoom-img">
</div>
<script>
let profiles = [];
let currentProfileId = null;
let items = [];
let isScanning = false;
const API_URL = "https://logs1.danielvu.com/ebay-price-check"
const fmat = (v) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(v || 0);
async function init() {
await fetchProfiles();
if (profiles.length > 0) {
switchProfile(profiles[0].id);
}
fetchStatus();
setInterval(fetchStatus, 3000);
}
async function fetchProfiles() {
const res = await fetch(API_URL+'/api/profiles');
profiles = await res.json();
renderTabs();
}
function renderTabs() {
const container = document.getElementById('profile-tabs');
container.innerHTML = profiles.map(p => `
<div class="tab ${currentProfileId == p.id ? 'active' : ''}" onclick="switchProfile(${p.id})">${p.name}</div>
`).join('') + `<div class="tab tab-add" onclick="openProfileModal()">+ New Profile</div>`;
}
async function switchProfile(id) {
if (!id) return;
currentProfileId = id;
renderTabs();
const p = profiles.find(x => x.id == id);
if (p) {
document.getElementById('last-run-time').innerText = p.last_scan_time ? new Date(p.last_scan_time).toLocaleString() : 'Never';
}
await fetchItems();
}
async function fetchItems() {
if (!currentProfileId) return;
const res = await fetch(API_URL+`/api/items?profile_id=${currentProfileId}`);
items = await res.json();
renderItems();
}
function renderItems() {
const q = document.getElementById('searchInput').value.toLowerCase();
const filtered = items.filter(i =>
i.title.toLowerCase().includes(q) ||
(i.partNumber && i.partNumber.toLowerCase().includes(q)) ||
(i.seller_username && i.seller_username.toLowerCase().includes(q))
);
document.getElementById('item-count').innerText = `Found ${filtered.length} items`;
const tbody = document.getElementById('item-tbody');
tbody.innerHTML = filtered.length ? filtered.map(item => {
const img = item.images && item.images.length > 0 ? item.images[0] : 'https://via.placeholder.com/64';
return `
<tr>
<td><img src="${img}" class="td-img" onclick="openZoom('${img}')"></td>
<td>
<div class="td-title" onclick="openDetail('${item.id}')">${item.title}</div>
<div class="td-sub">
ID: ${item.id} | PN: ${item.partNumber || 'N/A'} | <b>${item.manufacturer || 'Generic'}</b>
${item.specs ? `<br><span style="color:var(--warning); font-size:0.75rem">⚠️ ${item.specs}</span>` : ''}
</div>
</td>
<td>
<div class="price-lg">${fmat(item.avgPrice)}</div>
<div class="td-sub">Qty: ${item.available} | Ship: ${fmat(item.shipping)}</div>
</td>
<td><span class="profit-badge">+${fmat(item.profit)}</span></td>
<td style="font-size: 0.8rem; max-width: 250px; color: #bae6fd">
${item.ai_suggestion ? '🤖 ' + item.ai_suggestion : '<i>Pending...</i>'}
</td>
<td>
<select onchange="updateStatus('${item.id}', this.value)" style="background:#0f1422; color:white; border:1px solid var(--border); padding:0.3rem; border-radius:4px; font-size:0.8rem">
<option value="waiting" selected>Waiting</option>
<option value="done">Done</option>
<option value="skip">Skip</option>
</select>
</td>
</tr>
`;
}).join('') : `<tr><td colspan="6" class="empty-state">No PASS items found for this profile.</td></tr>`;
}
async function fetchStatus() {
const res = await fetch(API_URL+'/api/status');
const data = await res.json();
isScanning = data.isScanning;
document.getElementById('status-dot').className = `status-dot ${isScanning ? 'active' : ''}`;
document.getElementById('status-text').innerText = isScanning ? 'Searching eBay...' : 'Ready';
document.getElementById('btn-scan').disabled = isScanning;
const progWrap = document.getElementById('progress-wrapper');
const progInfo = document.getElementById('progress-info');
const progFill = document.getElementById('progress-fill');
if (isScanning && data.scanProgress && data.scanProgress.total > 0) {
progWrap.style.display = 'block';
progInfo.style.display = 'block';
const pct = Math.round((data.scanProgress.current / data.scanProgress.total) * 100);
progFill.style.width = `${pct}%`;
progInfo.innerText = `Scanning ${data.scanProgress.profileName}: ${data.scanProgress.current}/${data.scanProgress.total} (${pct}%)`;
if (pct === 100) {
progInfo.innerText = "Finishing scan...";
}
} else {
progWrap.style.display = 'none';
progInfo.style.display = 'none';
}
}
async function triggerScan(target) {
const pid = target === 'all' ? 'all' : currentProfileId;
if (!pid) return;
await fetch(API_URL+'/api/scan', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ profile_id: pid })
});
fetchStatus();
}
async function updateStatus(id, status) {
await fetch(API_URL+`/api/items/${id}/status`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ status })
});
items = items.filter(i => i.id !== id);
renderItems();
}
// Modal Control
function closeModals() {
document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active'));
}
function openZoom(url) {
document.getElementById('zoom-img').src = url;
document.getElementById('zoom-overlay').classList.add('active');
}
let editingProfileId = null;
let editingKeywordId = null;
// Profile Management
function openProfileModal() {
editingProfileId = null;
document.getElementById('new-profile-name').value = '';
document.getElementById('new-profile-ratio').value = '0.85';
document.getElementById('btn-save-profile').innerText = 'Add Profile';
document.getElementById('modal-profiles').classList.add('active');
renderProfileList();
}
function renderProfileList() {
document.getElementById('profile-list').innerHTML = profiles.map(p => {
const lastScan = p.last_scan_time ? new Date(p.last_scan_time).toLocaleString() : 'Never';
return `
<div style="display:flex; justify-content:space-between; align-items:center; padding:0.75rem; border-bottom:1px solid var(--border)">
<div>
<b>${p.name}</b> (Ratio: ${p.price_ratio})<br>
<small style="color:var(--text-muted)">Last scan: ${lastScan}</small>
</div>
<div style="display:flex; gap:0.5rem">
<button class="btn-outline" style="padding:0.3rem 0.6rem" onclick="editProfile(${p.id}, '${p.name}', ${p.price_ratio})">Edit</button>
<button class="btn-danger" style="padding:0.3rem 0.6rem" onclick="deleteProfile(${p.id})">Delete</button>
</div>
</div>
`;}).join('');
}
function editProfile(id, name, ratio) {
editingProfileId = id;
document.getElementById('new-profile-name').value = name;
document.getElementById('new-profile-ratio').value = ratio;
document.getElementById('btn-save-profile').innerText = 'Update Profile';
}
async function saveProfile() {
const name = document.getElementById('new-profile-name').value;
const ratio = document.getElementById('new-profile-ratio').value;
if (!name) return;
const method = editingProfileId ? 'PUT' : 'POST';
const url = editingProfileId ? API_URL+`/api/profiles/${editingProfileId}` : API_URL+'/api/profiles';
await fetch(url, {
method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name, price_ratio: parseFloat(ratio) })
});
editingProfileId = null;
document.getElementById('new-profile-name').value = '';
document.getElementById('btn-save-profile').innerText = 'Add Profile';
await fetchProfiles();
renderProfileList();
}
async function deleteProfile(id) {
if (!confirm('Delete this profile and ALL associated data?')) return;
await fetch(API_URL+`/api/profiles/${id}`, { method: 'DELETE' });
await fetchProfiles();
if (currentProfileId == id) currentProfileId = profiles[0]?.id || null;
renderProfileList();
renderTabs();
}
// Keywords Management
async function openKeywordsModal() {
if (!currentProfileId) return;
editingKeywordId = null;
resetKeywordForm();
const profile = profiles.find(p => p.id == currentProfileId);
document.getElementById('kw-modal-title').innerText = `Search Targets for: ${profile.name}`;
document.getElementById('modal-keywords').classList.add('active');
await fetchKeywords();
}
function resetKeywordForm() {
editingKeywordId = null;
document.getElementById('kw-part').value = '';
document.getElementById('kw-list').value = '';
document.getElementById('kw-price').value = '';
document.getElementById('btn-add-kw').innerText = 'Add Target to Profile';
document.getElementById('btn-add-kw').className = 'btn-success';
}
let currentKeywordsData = [];
async function fetchKeywords() {
const res = await fetch(API_URL+`/api/profiles/${currentProfileId}/keywords`);
currentKeywordsData = await res.json();
const tbody = document.getElementById('kw-tbody');
tbody.innerHTML = currentKeywordsData.map(kw => `
<tr>
<td>${kw.part_number || '-'}</td>
<td>${kw.keywords.map(k => `<span class="keyword-pill">${k}</span>`).join('')}</td>
<td>${fmat(kw.target_price)}</td>
<td>
<div style="display:flex; gap:0.5rem">
<button class="btn-outline" style="padding:0.2rem 0.5rem" onclick="editKeyword(${kw.id})">Edit</button>
<button class="btn-danger" style="padding:0.2rem 0.5rem" onclick="deleteKeyword(${kw.id})">Remove</button>
</div>
</td>
</tr>
`).join('');
}
function editKeyword(id) {
const kw = currentKeywordsData.find(k => k.id == id);
if (!kw) return;
editingKeywordId = id;
document.getElementById('kw-part').value = kw.part_number;
document.getElementById('kw-list').value = kw.keywords.join(', ');
document.getElementById('kw-price').value = kw.target_price;
document.getElementById('btn-add-kw').innerText = 'Update Target';
document.getElementById('btn-add-kw').className = 'btn-primary';
document.getElementById('single-import-form').scrollIntoView({ behavior: 'smooth' });
}
async function addKeyword() {
const part = document.getElementById('kw-part').value;
const list = document.getElementById('kw-list').value;
const price = document.getElementById('kw-price').value;
if (!list || !price) return;
const keywords = list.split(',').map(s => s.trim()).filter(Boolean);
const method = editingKeywordId ? 'PUT' : 'POST';
const url = editingKeywordId ? API_URL+`/api/keywords/${editingKeywordId}` : API_URL+`/api/profiles/${currentProfileId}/keywords`;
await fetch(url, {
method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ part_number: part, keywords, target_price: parseFloat(price) })
});
resetKeywordForm();
await fetchKeywords();
}
async function deleteKeyword(id) {
await fetch(API_URL+`/api/keywords/${id}`, { method: 'DELETE' });
await fetchKeywords();
}
function toggleBulkImport() {
const single = document.getElementById('single-import-form');
const bulk = document.getElementById('bulk-import-form');
if (single.style.display === 'none') {
single.style.display = 'block';
bulk.style.display = 'none';
} else {
single.style.display = 'none';
bulk.style.display = 'block';
}
}
async function processBulkKeywords() {
const text = document.getElementById('bulk-text').value;
const lines = text.split('\n').filter(l => l.trim().includes('|'));
const items = lines.map(line => {
const parts = line.split('|').map(s => s.trim());
if (parts.length < 3) return null;
let kwPart = parts[1];
// Only split by comma. Keep quotes as part of the query string.
let keywords = kwPart.split(',').map(s => s.trim()).filter(Boolean);
return {
part_number: (parts[0] === 'NONE' || parts[0] === '' || parts[0] === '""') ? '' : parts[0].replace(/"/g, ''),
keywords,
target_price: parseFloat(parts[2].replace(/[$,]/g, ''))
};
}).filter(Boolean);
if (!items.length) {
alert('No valid lines found. Format: PN | K1, K2 | Price');
return;
}
await fetch(API_URL+`/api/profiles/${currentProfileId}/keywords/bulk`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ items })
});
document.getElementById('bulk-text').value = '';
toggleBulkImport();
await fetchKeywords();
}
// Detail View
function openDetail(id) {
const item = items.find(i => i.id == id);
if (!item) return;
const detail = item.detail_response;
let descHtml = 'No description available.';
if (detail) descHtml = detail.shortDescription || detail.description || descHtml;
const body = document.getElementById('detail-body');
body.innerHTML = `
<div class="m-grid">
<div style="display:flex; flex-direction:column; gap:1rem">
<div style="width:100%; height:500px; display:flex; justify-content:center; align-items:center; background:#000; border-radius:12px; overflow:hidden">
<img src="${item.images[0] || ''}" style="max-width:100%; max-height:100%; object-fit:contain">
</div>
<div style="display:flex; gap:0.5rem; overflow-x:auto">
${item.images.slice(1,5).map(img => `<img src="${img}" style="width:60px; height:60px; object-fit:cover; border-radius:4px">`).join('')}
</div>
<div style="margin-top:0.5rem">
<h3 style="margin-bottom:0.5rem">Description</h3>
<div style="font-size:0.85rem; color:var(--text-muted); line-height:1.6; max-height:300px; overflow-y:auto; padding-right:1rem">
${descHtml}
</div>
</div>
</div>
<div>
<h2 style="margin-bottom:0.5rem">${item.title}</h2>
<div style="color:var(--text-muted); font-size:0.9rem; margin-bottom:1.5rem">ID: ${item.id}</div>
<div class="ai-box">
<b>🤖 AI Analysis:</b><br>${item.ai_suggestion || 'Not analyzed yet'}
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem; margin-bottom:2rem">
<div class="card" style="padding:1rem">
<div style="font-size:0.8rem; color:var(--text-muted)">Avg Price (Total / Qty)</div>
<div class="price-lg">${fmat(item.avgPrice)}</div>
<div style="font-size:0.75rem; color:var(--text-muted)">${fmat(item.total)} / ${item.qty}</div>
</div>
<div class="card" style="padding:1rem">
<div style="font-size:0.8rem; color:var(--text-muted)">Est. Profit</div>
<div class="price-lg" style="color:var(--success)">+${fmat(item.profit)}</div>
</div>
</div>
<div style="font-size:0.9rem">
<p><b>Seller:</b> ${item.seller_username || 'N/A'} (${item.seller_feedback_percent}%)</p>
<p><b>Available:</b> ${item.available} units</p>
<p><b>Target Price:</b> ${fmat(item.targetPrice)}</p>
<p><b>Threshold:</b> ${fmat(item.threshold)}</p>
</div>
<div style="margin-top:2.5rem; display:flex; gap:1rem">
<a href="${item.url}" target="_blank" class="btn-primary" style="flex:1; text-align:center; text-decoration:none; padding:0.8rem">Open on eBay</a>
<button class="btn-outline" style="flex:1" onclick="updateStatus('${item.id}', 'skip')">Skip</button>
<button class="btn-success" style="flex:1" onclick="updateStatus('${item.id}', 'done')">Done</button>
</div>
</div>
</div>
`;
document.getElementById('modal-detail').classList.add('active');
}
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeModals(); });
init();
</script>
</body>
</html>