ebayDeepScan/public/index.html

870 lines
36 KiB
HTML
Raw Permalink 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; }
.thumb-img {
width: 60px; height: 60px; object-fit: cover;
border-radius: 6px; cursor: pointer; border: 2px solid transparent;
transition: all 0.2s;
}
.thumb-img:hover { border-color: var(--primary); transform: translateY(-2px); }
.thumb-img.active { border-color: var(--primary); }
#detail-main-img {
max-width: 100%; max-height: 100%; object-fit: contain; cursor: zoom-in;
}
.row-highlight {
background: rgba(34, 197, 94, 0.1) !important;
border-left: 4px solid var(--success) !important;
}
.toolbar-group {
display: flex; gap: 0.5rem; align-items: center;
background: var(--surface); border: 1px solid var(--border);
padding: 0.3rem 0.6rem; border-radius: 8px;
}
.toolbar-group label { margin-bottom: 0; font-size: 0.75rem; white-space: nowrap; color: var(--text-muted); }
.toolbar-group input { border: none; width: 60px; padding: 0.2rem; background: transparent; color: var(--text); outline: none; }
.badge-offer {
background: #f97316; /* Orange */
color: white; padding: 0.1rem 0.4rem; border-radius: 4px;
font-size: 0.7rem; font-weight: 700; margin-left: 0.5rem;
display: inline-block; vertical-align: middle; line-height: 1;
}
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);
}
/* Bulk Bar */
.bulk-bar {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
background: var(--primary); color: white; padding: 0.8rem 2rem;
border-radius: 50px; display: flex; align-items: center; gap: 1.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 1001;
opacity: 0; pointer-events: none; transition: all 0.3s;
}
.bulk-bar.active { opacity: 1; pointer-events: all; bottom: 40px; }
.bulk-bar button {
background: rgba(255,255,255,0.15); color: white;
border: 1px solid rgba(255,255,255,0.3); padding: 0.5rem 1.2rem;
}
.bulk-bar button:hover { background: rgba(255,255,255,0.25); }
.item-checkbox { width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary); }
</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 class="toolbar-group">
<label>Profit Min ($):</label>
<input type="number" id="minProfit" value="0" oninput="renderItems()">
</div>
<div class="toolbar-group">
<label>Max:</label>
<input type="number" id="maxProfit" placeholder="Any" oninput="renderItems()">
</div>
<div id="item-count" style="font-size: 0.85rem; color: var(--text-muted)"></div>
</div>
<div class="card">
<table>
<thead>
<tr>
<th width="40"><input type="checkbox" id="selectAll" onclick="toggleSelectAll(this.checked)"></th>
<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" style="grid-template-columns: 1fr 1fr 2fr">
<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>
<label>Common Keywords (appended to all queries)</label>
<input type="text" id="new-profile-common" placeholder='-laptop "New In Box"'>
</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>
<!-- Bulk Action Bar -->
<div class="bulk-bar" id="bulk-bar">
<span id="selected-count" style="font-weight: 600; font-family: var(--mono); font-size: 0.9rem"></span>
<div style="display: flex; gap: 0.5rem">
<button onclick="bulkUpdateStatus('done')">Mark Done</button>
<button onclick="bulkUpdateStatus('skip')">Mark Skip</button>
<button class="btn-outline" style="background:none; border:none; padding:0; text-decoration:underline; font-size:0.8rem; margin-left:0.5rem" onclick="toggleSelectAll(false)">Cancel</button>
</div>
</div>
<script>
let profiles = [];
let currentProfileId = null;
let items = [];
let isScanning = false;
// const API_URL = "http://localhost:4000"
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 + 'Z').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 minP = parseFloat(document.getElementById('minProfit').value || -999999);
const maxP = parseFloat(document.getElementById('maxProfit').value || 999999);
const filtered = items.filter(i => {
const matchesQuery = i.title.toLowerCase().includes(q) ||
(i.partNumber && i.partNumber.toLowerCase().includes(q)) ||
(i.seller_username && i.seller_username.toLowerCase().includes(q));
const matchesProfit = i.profit >= minP && i.profit <= maxP;
return matchesQuery && matchesProfit;
});
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';
const isHot = item.ai_suggestion && item.ai_suggestion.includes('Hãy mua ngay');
const hasOffer = item.detail_response?.buyingOptions?.includes('BEST_OFFER');
const offerBadge = hasOffer ? `<span class="badge-offer">OFFER</span>` : '';
return `
<tr class="${isHot ? 'row-highlight' : ''}">
<td><input type="checkbox" class="item-checkbox" value="${item.id}" onchange="updateBulkBar()"></td>
<td><img src="${img}" class="td-img" onclick="openZoom('${img}')"></td>
<td>
<div class="td-title" onclick="openDetail('${item.id}')">${item.title}${offerBadge}</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'));
}
// Bulk Management
function toggleSelectAll(checked) {
document.querySelectorAll('.item-checkbox').forEach(cb => {
cb.checked = checked;
});
updateBulkBar();
}
function updateBulkBar() {
const selected = document.querySelectorAll('.item-checkbox:checked');
const bar = document.getElementById('bulk-bar');
const count = document.getElementById('selected-count');
if (selected.length > 0) {
count.innerText = `${selected.length} items selected`;
bar.classList.add('active');
} else {
bar.classList.remove('active');
document.getElementById('selectAll').checked = false;
}
}
async function bulkUpdateStatus(status) {
const selected = Array.from(document.querySelectorAll('.item-checkbox:checked')).map(cb => cb.value);
if (selected.length === 0) return;
if (!confirm(`Mark ${selected.length} items as ${status}?`)) return;
const res = await fetch(API_URL + '/api/items/bulk-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: selected, status })
});
const data = await res.json();
if (data.success) {
items = items.filter(i => !selected.includes(i.id));
renderItems();
document.getElementById('bulk-bar').classList.remove('active');
document.getElementById('selectAll').checked = false;
}
}
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('new-profile-common').value = '';
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 + 'Z').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>
${p.common_keywords ? `<small style="color:var(--text-muted)">Common: ${p.common_keywords}</small><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})">Edit</button>
<button class="btn-danger" style="padding:0.3rem 0.6rem" onclick="deleteProfile(${p.id})">Delete</button>
</div>
</div>
`;}).join('');
}
function editProfile(id) {
const p = profiles.find(x => x.id === id);
if (!p) return;
editingProfileId = id;
document.getElementById('new-profile-name').value = p.name;
document.getElementById('new-profile-ratio').value = p.price_ratio;
document.getElementById('new-profile-common').value = p.common_keywords || '';
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;
const common = document.getElementById('new-profile-common').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),
common_keywords: common
})
});
editingProfileId = null;
document.getElementById('new-profile-name').value = '';
document.getElementById('new-profile-common').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],
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 id="detail-main-img" src="${item.images[0] || ''}" onclick="openZoom(this.src)">
</div>
<div style="display:flex; gap:0.5rem; overflow-x:auto; padding-bottom: 0.5rem">
${item.images.map((img, idx) => `
<img class="thumb-img ${idx === 0 ? 'active' : ''}"
src="${img}"
onclick="document.getElementById('detail-main-img').src='${img}'; document.querySelectorAll('.thumb-img').forEach(el=>el.classList.remove('active')); this.classList.add('active')">
`).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}${item.detail_response?.buyingOptions?.includes('BEST_OFFER') ? '<span class="badge-offer">OFFER</span>' : ''}</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'); closeModals()">Skip</button>
<button class="btn-success" style="flex:1" onclick="updateStatus('${item.id}', 'done'); closeModals()">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>