← back to Vendor Discount Agent
public/index.html
339 lines
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vendor Discount Agent</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0a0e17; color: #e0e0e0; padding: 20px; }
h1 { color: #00d4aa; margin-bottom: 5px; font-size: 1.6em; }
.subtitle { color: #666; margin-bottom: 20px; font-size: 0.9em; }
.stats-bar { display: flex; gap: 15px; margin-bottom: 25px; flex-wrap: wrap; }
.stat { background: #141a28; border: 1px solid #1e2a3a; border-radius: 8px; padding: 12px 18px; min-width: 140px; }
.stat .num { font-size: 1.8em; font-weight: 700; color: #00d4aa; }
.stat .label { font-size: 0.75em; color: #888; text-transform: uppercase; letter-spacing: 1px; }
.stat.warning .num { color: #ff6b6b; }
.actions { margin-bottom: 20px; display: flex; gap: 10px; }
button { background: #00d4aa; color: #0a0e17; border: none; padding: 10px 20px; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 0.9em; }
button:hover { background: #00f0c0; }
button:disabled { background: #333; color: #666; cursor: not-allowed; }
button.secondary { background: #1e2a3a; color: #00d4aa; }
button.secondary:hover { background: #2a3a4a; }
table { width: 100%; border-collapse: collapse; background: #141a28; border-radius: 8px; overflow: hidden; }
th { background: #1a2235; color: #00d4aa; text-align: left; padding: 10px 12px; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 8px 12px; border-bottom: 1px solid #1e2a3a; font-size: 0.85em; }
tr:hover { background: #1a2235; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; font-weight: 600; }
.badge-green { background: #0a3a2a; color: #00d4aa; }
.badge-red { background: #3a1a1a; color: #ff6b6b; }
.badge-yellow { background: #3a3a1a; color: #ffd93d; }
.badge-blue { background: #1a2a3a; color: #64b5f6; }
.discount-input { width: 60px; background: #0a0e17; border: 1px solid #1e2a3a; color: #e0e0e0; padding: 4px 6px; border-radius: 4px; text-align: right; }
.notes-input { width: 200px; background: #0a0e17; border: 1px solid #1e2a3a; color: #e0e0e0; padding: 4px 6px; border-radius: 4px; }
.save-btn { background: #1e2a3a; color: #00d4aa; border: 1px solid #00d4aa; padding: 3px 10px; border-radius: 4px; font-size: 0.75em; cursor: pointer; }
.save-btn:hover { background: #00d4aa; color: #0a0e17; }
.check-btn { background: #1a2a3a; color: #64b5f6; border: 1px solid #64b5f6; padding: 3px 10px; border-radius: 4px; font-size: 0.75em; cursor: pointer; }
.check-btn:hover { background: #64b5f6; color: #0a0e17; }
.tabs { display: flex; gap: 5px; margin-bottom: 15px; }
.tab { padding: 8px 16px; background: #141a28; border: 1px solid #1e2a3a; border-radius: 6px 6px 0 0; cursor: pointer; color: #888; }
.tab.active { background: #1a2235; color: #00d4aa; border-bottom-color: #1a2235; }
.panel { display: none; }
.panel.active { display: block; }
.toast { position: fixed; bottom: 20px; right: 20px; background: #00d4aa; color: #0a0e17; padding: 12px 20px; border-radius: 6px; font-weight: 600; display: none; z-index: 100; }
</style>
</head>
<body>
<h1>Vendor Discount Agent</h1>
<p class="subtitle">Login to vendor trade portals — verify net discount on products</p>
<div class="stats-bar" id="stats"></div>
<div class="tabs">
<div class="tab active" data-tab="vendors">All Vendors</div>
<div class="tab" data-tab="ready">Ready to Check</div>
<div class="tab" data-tab="checks">Check History</div>
</div>
<div class="panel active" id="panel-vendors">
<div class="actions">
<button class="secondary" id="btn-refresh-vendors">Refresh</button>
</div>
<table id="vendor-table">
<thead>
<tr>
<th>Vendor</th>
<th>Products</th>
<th>Credentials</th>
<th>Discount %</th>
<th>Unit</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="vendor-tbody"></tbody>
</table>
</div>
<div class="panel" id="panel-ready">
<div class="actions">
<button id="btn-check-all">Check All Ready Vendors</button>
<button class="secondary" id="btn-refresh-ready">Refresh</button>
</div>
<table id="ready-table">
<thead>
<tr>
<th>Vendor</th>
<th>Products</th>
<th>Login URL</th>
<th>Current Discount</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="ready-tbody"></tbody>
</table>
</div>
<div class="panel" id="panel-checks">
<div class="actions">
<button class="secondary" id="btn-refresh-checks">Refresh</button>
</div>
<table id="checks-table">
<thead>
<tr>
<th>Vendor</th>
<th>Checked</th>
<th>Status</th>
<th>Login</th>
<th>Discount</th>
<th>Sample Product</th>
<th>List / Net</th>
<th>Notes</th>
</tr>
</thead>
<tbody id="checks-tbody"></tbody>
</table>
</div>
<div class="toast" id="toast"></div>
<script>
// Safe DOM helpers
function el(tag, attrs, children) {
const e = document.createElement(tag);
if (attrs) Object.entries(attrs).forEach(([k, v]) => {
if (k === 'className') e.className = v;
else if (k === 'textContent') e.textContent = v;
else if (k.startsWith('data')) e.setAttribute(k.replace(/([A-Z])/g, '-$1').toLowerCase(), v);
else e.setAttribute(k, v);
});
if (children) {
if (typeof children === 'string') e.textContent = children;
else if (Array.isArray(children)) children.forEach(c => { if (c) e.appendChild(c); });
}
return e;
}
function clearChildren(node) { while (node.firstChild) node.removeChild(node.firstChild); }
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
const panelId = 'panel-' + tab.getAttribute('data-tab');
document.getElementById(panelId).classList.add('active');
const name = tab.getAttribute('data-tab');
if (name === 'ready') loadReady();
if (name === 'checks') loadChecks();
});
});
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 3000);
}
async function loadStats() {
const res = await fetch('/api/stats');
const s = await res.json();
const container = document.getElementById('stats');
clearChildren(container);
const statData = [
{ num: s.total_active, label: 'Active Vendors', warn: false },
{ num: s.has_discount, label: 'Have Discount', warn: false },
{ num: s.missing_discount, label: 'Missing Discount', warn: true },
{ num: s.has_full_creds, label: 'Full Credentials', warn: false }
];
statData.forEach(sd => {
const div = el('div', { className: 'stat' + (sd.warn ? ' warning' : '') });
const numDiv = el('div', { className: 'num' }, String(sd.num));
const labelDiv = el('div', { className: 'label' }, sd.label);
div.appendChild(numDiv);
div.appendChild(labelDiv);
container.appendChild(div);
});
}
function createBadge(text, type) {
return el('span', { className: 'badge badge-' + type }, text);
}
async function loadVendors() {
const res = await fetch('/api/vendors');
const vendors = await res.json();
const tbody = document.getElementById('vendor-tbody');
clearChildren(tbody);
vendors.forEach(v => {
const tr = el('tr');
// Vendor name
const td1 = el('td');
td1.appendChild(el('strong', {}, v.vendor_name));
td1.appendChild(el('br'));
const codeSpan = el('span', { style: 'color:#666;font-size:0.8em' }, v.vendor_code);
td1.appendChild(codeSpan);
tr.appendChild(td1);
// Products
tr.appendChild(el('td', {}, String(v.catalog_count || 0)));
// Credentials
const td3 = el('td');
if (v.has_password) td3.appendChild(createBadge('Ready', 'green'));
else if (v.has_credentials) td3.appendChild(createBadge('Partial', 'yellow'));
else td3.appendChild(createBadge('None', 'red'));
tr.appendChild(td3);
// Discount input
const td4 = el('td');
const discInput = el('input', {
className: 'discount-input', type: 'number', step: '0.5',
value: v.vendor_discount_pct || '', placeholder: '\u2014'
});
discInput.dataset.code = v.vendor_code;
td4.appendChild(discInput);
tr.appendChild(td4);
// Unit
tr.appendChild(el('td', {}, v.pricing_unit || 'yard'));
// Notes input
const td6 = el('td');
const notesInput = el('input', {
className: 'notes-input', value: v.pricing_notes || '', placeholder: 'pricing notes...'
});
notesInput.dataset.code = v.vendor_code;
td6.appendChild(notesInput);
tr.appendChild(td6);
// Actions
const td7 = el('td');
const saveBtn = el('button', { className: 'save-btn' }, 'Save');
saveBtn.addEventListener('click', () => saveDiscount(v.vendor_code, tr));
td7.appendChild(saveBtn);
if (v.has_password) {
const chkBtn = el('button', { className: 'check-btn', style: 'margin-left:5px' }, 'Check');
chkBtn.addEventListener('click', () => checkVendor(v.vendor_code));
td7.appendChild(chkBtn);
}
tr.appendChild(td7);
tbody.appendChild(tr);
});
}
async function loadReady() {
const res = await fetch('/api/vendors/ready');
const vendors = await res.json();
const tbody = document.getElementById('ready-tbody');
clearChildren(tbody);
vendors.forEach(v => {
const tr = el('tr');
tr.appendChild(el('td', {}, v.vendor_name));
tr.appendChild(el('td', {}, String(v.catalog_count || 0)));
const urlTd = el('td', { style: 'font-size:0.8em;max-width:250px;overflow:hidden;text-overflow:ellipsis' }, v.login_url || '\u2014');
tr.appendChild(urlTd);
const discTd = el('td');
if (v.vendor_discount_pct) discTd.textContent = v.vendor_discount_pct + '%';
else discTd.appendChild(createBadge('Unknown', 'red'));
tr.appendChild(discTd);
const actTd = el('td');
const btn = el('button', { className: 'check-btn' }, 'Check Now');
btn.addEventListener('click', () => checkVendor(v.vendor_code));
actTd.appendChild(btn);
tr.appendChild(actTd);
tbody.appendChild(tr);
});
}
async function loadChecks() {
const res = await fetch('/api/checks');
const checks = await res.json();
const tbody = document.getElementById('checks-tbody');
clearChildren(tbody);
checks.forEach(c => {
const tr = el('tr');
tr.appendChild(el('td', {}, c.vendor_name));
tr.appendChild(el('td', { style: 'font-size:0.8em' }, new Date(c.checked_at).toLocaleString()));
const statusTd = el('td');
const statusType = c.status === 'discount_calculated' ? 'green' :
(c.status === 'error' || c.status === 'login_failed') ? 'red' : 'yellow';
statusTd.appendChild(createBadge(c.status, statusType));
tr.appendChild(statusTd);
tr.appendChild(el('td', {}, c.login_success ? '\u2713' : '\u2717'));
tr.appendChild(el('td', {}, c.calculated_discount ? c.calculated_discount + '%' : '\u2014'));
tr.appendChild(el('td', { style: 'font-size:0.8em;max-width:200px;overflow:hidden' }, c.sample_product || '\u2014'));
const priceTd = el('td');
priceTd.textContent = c.sample_list_price ? '$' + c.sample_list_price + ' / $' + c.sample_net_price : '\u2014';
tr.appendChild(priceTd);
const notesTd = el('td', { style: 'font-size:0.8em;max-width:200px;overflow:hidden' });
notesTd.textContent = c.error || (c.notes ? c.notes.substring(0, 80) : '\u2014');
tr.appendChild(notesTd);
tbody.appendChild(tr);
});
}
async function saveDiscount(code, row) {
const discount = row.querySelector('.discount-input').value;
const notes = row.querySelector('.notes-input').value;
await fetch('/api/vendors/' + encodeURIComponent(code) + '/discount', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ discount_pct: discount || null, pricing_notes: notes })
});
toast('Saved discount for ' + code);
loadStats();
}
async function checkVendor(code) {
toast('Starting discount check for ' + code + '...');
await fetch('/api/check/' + encodeURIComponent(code), { method: 'POST' });
}
async function checkAll() {
if (!confirm('Run discount checks on all vendors with credentials?')) return;
toast('Starting all vendor checks...');
const res = await fetch('/api/check-all', { method: 'POST' });
const data = await res.json();
toast('Checking ' + data.count + ' vendors...');
}
// Wire up buttons
document.getElementById('btn-refresh-vendors').addEventListener('click', loadVendors);
document.getElementById('btn-refresh-ready').addEventListener('click', loadReady);
document.getElementById('btn-refresh-checks').addEventListener('click', loadChecks);
document.getElementById('btn-check-all').addEventListener('click', checkAll);
// Init
loadStats();
loadVendors();
</script>
</body>
</html>