← back to AbramsOS

views/reviews.ejs

112 lines

<%- include('partials/header', { title: 'Reviews' }) %>

<section class="page-head">
  <h1>Reviews &amp; Thank-yous</h1>
  <div class="grid-controls">
    <button id="genBtn" class="btn">Generate drafts</button>
    <label>Show
      <select id="filterSel">
        <option value="needs">Needs approval</option>
        <option value="approved">Approved</option>
        <option value="all" selected>All</option>
      </select>
    </label>
    <label>Density
      <input id="densityRange" type="range" min="180" max="480" step="10" value="320">
    </label>
  </div>
</section>

<section class="reviews-bar glass" style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;padding:.6rem .9rem;margin-bottom:.8rem;">
  <label style="display:flex;gap:.35rem;align-items:center;"><input type="checkbox" id="selAll"> Select all</label>
  <button id="approveBtn" class="btn">Approve selected</button>
  <button id="postBtn" class="btn" title="Posts per-item, one at a time">Post approved</button>
  <span class="subtle">
    <% const t = summary.byTarget || {}; %>
    amazon_review: <%= (t.amazon_review||{}).draft||0 %> draft / <%= (t.amazon_review||{}).approved||0 %> appr ·
    seller_email: <%= (t.seller_email||{}).draft||0 %> / <%= (t.seller_email||{}).approved||0 %> ·
    <strong>posted: <%= summary.posted||0 %></strong>
  </span>
  <span class="confidence" title="Live executor switch">
    executors: <%= executorsLive ? 'ARMED (per-item still gated)' : 'OFF (drafts only)' %>
  </span>
</section>

<% if (!items.length) { %>
  <section class="empty glass"><p>No purchases yet. <a href="/connectors">Sync a connector</a> to ingest receipts, then Generate drafts.</p></section>
<% } else { %>
  <section id="reviewGrid" class="review-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.8rem;">
    <% items.forEach(p => { %>
      <article class="review-card glass" style="padding:.8rem;display:flex;flex-direction:column;gap:.5rem;">
        <header style="display:flex;justify-content:space-between;gap:.5rem;">
          <h3 style="margin:0;font-size:1rem;"><%= p.product || p.merchant_name %></h3>
          <span class="subtle" style="font-size:.72rem;"><%= p.merchant_name %></span>
        </header>
        <div class="when" title="<%= new Date(p.purchase_date).toISOString() %>" style="font-size:.72rem;opacity:.7;">
          🕓 <%= new Date(p.purchase_date).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}) %>
          <% if (p.total_amount) { %> · <%= p.currency||'USD' %> <%= Number(p.total_amount).toFixed(2) %><% } %>
        </div>
        <% if (!p.drafts.length) { %>
          <p class="subtle" style="font-size:.8rem;">No drafts yet — click <em>Generate drafts</em>.</p>
        <% } %>
        <% p.drafts.forEach(d => { %>
          <div class="draft" data-id="<%= d.id %>" data-status="<%= d.status %>" data-target="<%= d.target %>"
               style="border:1px solid var(--hair,#3333);border-radius:8px;padding:.5rem;display:flex;flex-direction:column;gap:.35rem;">
            <div style="display:flex;justify-content:space-between;align-items:center;gap:.4rem;">
              <label style="display:flex;gap:.4rem;align-items:center;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;">
                <input type="checkbox" class="draftChk" value="<%= d.id %>" <%= d.status==='posted'?'disabled':'' %>>
                <%= d.target.replace('_',' ') %>
              </label>
              <span class="pill pill-<%= d.status %>" style="font-size:.68rem;padding:.1rem .45rem;border-radius:999px;border:1px solid currentColor;opacity:.85;"><%= d.status %></span>
            </div>
            <% if (d.draft_title) { %><strong style="font-size:.85rem;"><%= d.draft_title %></strong><% } %>
            <div style="font-size:.82rem;line-height:1.35;white-space:pre-wrap;"><%= d.draft_text %></div>
            <% if (d.target === 'seller_email') { %>
              <div style="display:flex;gap:.35rem;align-items:center;font-size:.72rem;">
                to: <input class="sellerTo" data-id="<%= d.id %>" value="<%= d.seller_email_to || '' %>" placeholder="seller/brand email (unresolved)" style="flex:1;font-size:.72rem;padding:.2rem;">
                <button class="btn saveTo" data-id="<%= d.id %>" style="font-size:.7rem;">save</button>
              </div>
            <% } %>
            <div style="display:flex;gap:.4rem;">
              <button class="btn skipBtn" data-id="<%= d.id %>" style="font-size:.7rem;">skip</button>
              <% if (d.status==='approved') { %><button class="btn unapproveBtn" data-id="<%= d.id %>" style="font-size:.7rem;">unapprove</button><% } %>
            </div>
          </div>
        <% }) %>
      </article>
    <% }) %>
  </section>
<% } %>

<script>
(function(){
  const $ = (s,r=document)=>r.querySelector(s);
  const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
  async function api(path, body){
    const r = await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})});
    return r.json();
  }
  const checked = ()=>$$('.draftChk:checked').map(c=>c.value);

  $('#genBtn').onclick = async ()=>{ $('#genBtn').disabled=true; const o=await api('/api/reviews/generate',{}); alert(`Generated: ${o.created||0} new, ${o.refreshed||0} refreshed, ${o.skippedLocked||0} locked (across ${o.purchases||0} purchases).`); location.reload(); };
  $('#selAll').onchange = e=>$$('.draftChk:not(:disabled)').forEach(c=>c.checked=e.target.checked);
  $('#approveBtn').onclick = async ()=>{ const ids=checked(); if(!ids.length) return alert('Select at least one draft.'); const o=await api('/api/reviews/approve-batch',{ids}); alert(`Approved ${o.approved||0}.`); location.reload(); };
  $('#postBtn').onclick = async ()=>{
    const ids=checked(); if(!ids.length) return alert('Select approved drafts to post.');
    if(!confirm(`Post ${ids.length} item(s), one at a time? Reviews open in Chrome for you to submit; only seller emails with an address send. Nothing fires unless executors are ARMED.`)) return;
    const o=await api('/api/reviews/post-approved',{ids, live:true});
    alert(o.live===false ? 'Executors are OFF — nothing sent. Arm REVIEWS_EXECUTORS_LIVE first.' : 'Results:\n'+o.results.map(r=>`${r.id}: ${r.status}${r.reason?' — '+r.reason:''}`).join('\n'));
    location.reload();
  };
  $$('.skipBtn').forEach(b=>b.onclick=async()=>{ await api('/api/reviews/skip',{id:b.dataset.id}); location.reload(); });
  $$('.unapproveBtn').forEach(b=>b.onclick=async()=>{ await api('/api/reviews/unapprove',{id:b.dataset.id}); location.reload(); });
  $$('.saveTo').forEach(b=>b.onclick=async()=>{ const to=$(`.sellerTo[data-id="${b.dataset.id}"]`).value; await api('/api/reviews/seller-email-to',{id:b.dataset.id,to}); location.reload(); });
  // filter
  $('#filterSel').onchange = e=>{ const v=e.target.value; $$('.draft').forEach(d=>{ const st=d.dataset.status; d.style.display = v==='all'||(v==='needs'&&st==='draft')||(v==='approved'&&st==='approved') ? '' : 'none'; }); };
  // density
  const dr=$('#densityRange'); if(dr) dr.oninput=e=>{ $('#reviewGrid').style.gridTemplateColumns=`repeat(auto-fill,minmax(${e.target.value}px,1fr))`; };
})();
</script>

<%- include('partials/footer', { }) %>