746 lines
31 KiB
HTML
746 lines
31 KiB
HTML
<!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" 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>
|
||
|
||
<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).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('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).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 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>
|