← 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 &mdash; 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>