[object Object]

← back to AbramsOS

settlements: 3 views (grid/list/table sortable) + one-click Claim it & Go (marks eligible + stages openclaw prefill + opens portal + prep card w/ identity+PayPal values); payment default PayPal->steveabramsdesigns

103c476261eed2f09551da73f77c7e394e08c411 · 2026-08-18 15:53:38 -0700 · Steve

Files touched

Diff

commit 103c476261eed2f09551da73f77c7e394e08c411
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 18 15:53:38 2026 -0700

    settlements: 3 views (grid/list/table sortable) + one-click Claim it & Go (marks eligible + stages openclaw prefill + opens portal + prep card w/ identity+PayPal values); payment default PayPal->steveabramsdesigns
---
 routes/settlements.js |  86 ++++++++++++++++++++
 views/settlements.ejs | 219 ++++++++++++++++++++++++++++++--------------------
 2 files changed, 220 insertions(+), 85 deletions(-)

diff --git a/routes/settlements.js b/routes/settlements.js
index 59375b2..d8696dc 100644
--- a/routes/settlements.js
+++ b/routes/settlements.js
@@ -6,6 +6,7 @@ const fs = require('fs');
 const path = require('path');
 const db = require('../lib/db');
 const filler = require('../lib/openclaw-claim-filler');
+const { extractAddress } = require('../lib/claims-autopilot');
 const { rankClaims } = require('../lib/settlement-score');
 
 const router = express.Router();
@@ -66,4 +67,89 @@ router.post('/api/settlements/:id/fill', async (req, res) => {
   } catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
 });
 
+// "Claim it & Go" — ONE call: mark eligible → stage the same openclaw prefill brief the /fill
+// route stages → return the prefill values + mode_url so the UI can open the official claim site
+// and show the values to paste. Fill-not-submit: this NEVER submits (staging only sets 'queued').
+router.post('/api/settlements/:id/claim-and-go', async (req, res) => {
+  try {
+    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
+    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
+    const claim = r.rows[0];
+    // 1) mark eligible
+    await db.query(`UPDATE settlement_claim SET eligibility_state='eligible',updated_at=now() WHERE id=$1 AND user_id=$2`, [claim.id, USER]);
+    claim.eligibility_state = 'eligible';
+    // 2) build the identity profile (self; address parsed from person.notes)
+    const person = await db.query(
+      `SELECT full_name,email,phone,notes FROM person WHERE user_id=$1 AND relation='self' LIMIT 1`, [USER]
+    ).then(x => x.rows[0] || {}).catch(() => ({}));
+    const profile = {
+      full_name: person.full_name || null,
+      email: person.email || null,
+      phone: person.phone || null,
+      address: extractAddress(person.notes),
+    };
+    // 3) same staging the /fill route does
+    filler.stage(claim, profile);
+    await db.query(`UPDATE settlement_claim SET fill_state='queued',updated_at=now() WHERE id=$1`, [claim.id]);
+    res.json({
+      ok: true,
+      mode_url: claim.mode_url || null,
+      prefill: { full_name: profile.full_name, email: profile.email, phone: profile.phone, address: profile.address },
+      note: 'prefilled — review & submit on the claim site; nothing was submitted for you',
+    });
+  } catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
+});
+
+// ---- profile (identity + payment) used to prefill every claim ----
+// Payment default: PayPal -> steveabramsdesigns@gmail.com (Steve's choice 2026-08-18).
+const PAYMENT_DEFAULT = { method: process.env.CLAIM_PAY_METHOD || 'PayPal', paypal_email: process.env.CLAIM_PAY_PAYPAL || 'steveabramsdesigns@gmail.com' };
+async function loadProfile() {
+  const r = await db.query(`SELECT full_name,email,phone,notes FROM person WHERE user_id=$1 AND relation='self' LIMIT 1`, [USER]).catch(() => ({ rows: [] }));
+  const p = r.rows[0] || {};
+  return {
+    full_name: p.full_name || null,
+    email: p.email || null,
+    phone: p.phone || null,
+    address: extractAddress(p.notes),
+    payment: PAYMENT_DEFAULT,
+  };
+}
+// The flat field->value map an autofiller (or the UI copy-card) uses for ANY administrator's form.
+// Keys are the near-universal labels the research found across A.B.Data/Angeion/Epiq/JND/etc.
+function fieldMap(profile) {
+  const a = (profile.address || '').match(/^(.*?),\s*([^,]+),\s*([A-Z]{2})\s*(\d{5})/) || [];
+  const parts = String(profile.full_name || '').trim().split(/\s+/);
+  return {
+    first_name: parts[0] || '', last_name: parts.slice(1).join(' ') || '',
+    full_name: profile.full_name || '',
+    street: a[1] || profile.address || '', city: a[2] || '', state: a[3] || '', zip: a[4] || '',
+    email: profile.email || '', phone: (profile.phone || '').replace(/\D/g, ''),
+    payment_method: profile.payment.method, paypal_email: profile.payment.paypal_email,
+  };
+}
+
+// ONE-CLICK "Claim it & Go": mark eligible + stage the openclaw prefill brief, and return
+// everything the UI needs to finish — the claim portal URL, the fill values, and the field map.
+// Never submits. Works for every claim (that's Steve's requirement: all have a finish path).
+router.post('/api/settlements/:id/claim-and-go', async (req, res) => {
+  try {
+    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
+    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
+    const claim = r.rows[0];
+    await db.query(`UPDATE settlement_claim SET eligibility_state='eligible',updated_at=now() WHERE id=$1`, [claim.id]);
+    claim.eligibility_state = 'eligible';
+    const profile = await loadProfile();
+    let staged = null;
+    try { staged = filler.stage(claim, profile).local; await db.query(`UPDATE settlement_claim SET fill_state='queued',updated_at=now() WHERE id=$1`, [claim.id]); } catch (_e) {}
+    res.json({
+      ok: true,
+      mode_url: claim.mode_url || claim.admin_url || null,
+      prefill: profile,
+      fields: fieldMap(profile),
+      staged,
+      note: 'Prefill staged. Opening the claim portal — enter/verify the values below, then YOU check the perjury attestation and submit. Nothing was submitted for you.',
+    });
+  } catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
+});
+
 module.exports = router;
diff --git a/views/settlements.ejs b/views/settlements.ejs
index cf5c639..78deb23 100644
--- a/views/settlements.ejs
+++ b/views/settlements.ejs
@@ -1,71 +1,66 @@
 <%- include('partials/header', { title: 'Settlements' }) %>
 <%
-  // ---- render helpers (server data already carries ratings/priority/tier/rank) ----
   var TIER = { high:{dot:'🔴',label:'HIGH',color:'#c0392b'}, med:{dot:'🟡',label:'MED',color:'#b8860b'}, low:{dot:'⚪',label:'LOW',color:'#8894a5'} };
   function money(c){ return c==null ? '—' : '$'+(c/100).toLocaleString('en-US'); }
   function proofLabel(s){ return s.no_claim_required ? 'AUTO' : (s.proof_required===false ? 'no proof' : (s.proof_required ? 'proof' : '?')); }
-  function deadlinePill(s){
-    if (s.days==null) return 'no deadline';
-    if (s.days<0) return 'expired '+(-s.days)+'d ago';
-    if (s.days===0) return 'DUE TODAY';
-    if (s.days===1) return 'due tomorrow';
-    return 'in '+s.days+'d';
-  }
-  // a labelled 0-5 rating bar
-  function bar(label, v){
-    var pct = Math.round((v/5)*100);
-    var col = v>=4 ? '#3fb950' : v>=2 ? '#d29922' : '#8894a5';
-    return '<div class="rbar" title="'+label+' '+v+'/5">'
-      + '<span class="rbar-l">'+label+'</span>'
-      + '<span class="rbar-track"><span class="rbar-fill" style="width:'+pct+'%;background:'+col+'"></span></span>'
-      + '<span class="rbar-v">'+v+'</span></div>';
-  }
+  function deadlinePill(s){ if (s.days==null) return 'no deadline'; if (s.days<0) return 'expired '+(-s.days)+'d ago'; if (s.days===0) return 'DUE TODAY'; if (s.days===1) return 'due tomorrow'; return 'in '+s.days+'d'; }
+  function bar(label, v){ var pct=Math.round((v/5)*100); var col=v>=4?'#3fb950':v>=2?'#d29922':'#8894a5'; return '<div class="rbar" title="'+label+' '+v+'/5"><span class="rbar-l">'+label+'</span><span class="rbar-track"><span class="rbar-fill" style="width:'+pct+'%;background:'+col+'"></span></span><span class="rbar-v">'+v+'</span></div>'; }
 %>
 <style>
+  .views{display:flex;gap:6px;margin:8px 0 4px}
+  .views button{background:transparent;color:var(--text-dim);border:1px solid rgba(255,255,255,.15);border-radius:7px;padding:5px 12px;cursor:pointer;font-size:13px}
+  .views button.on{background:#2d5bff;color:#fff;border-color:#2d5bff}
   .claim-card{border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:14px 16px;margin:10px 0;display:grid;grid-template-columns:44px 1fr 300px;gap:14px;align-items:start}
   .claim-card.expired{opacity:.5}
   .rank-badge{display:flex;flex-direction:column;align-items:center;justify-content:center;width:44px;height:44px;border-radius:10px;background:rgba(255,255,255,.05);font-weight:700}
-  .rank-badge .n{font-size:18px;line-height:1}
-  .rank-badge .p{font-size:9px;color:var(--text-dim);margin-top:2px}
+  .rank-badge .n{font-size:18px;line-height:1}.rank-badge .p{font-size:9px;color:var(--text-dim);margin-top:2px}
   .tier-chip{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;border:1px solid currentColor}
   .pill{display:inline-block;font-size:11px;padding:2px 8px;border-radius:20px;background:rgba(255,255,255,.06);margin-right:6px}
   .pill.due{color:#f0b429}.pill.today{color:#ff6b6b;font-weight:700}.pill.past{color:#8894a5}
   .rbar{display:flex;align-items:center;gap:6px;margin:3px 0;font-size:11px}
-  .rbar-l{width:78px;color:var(--text-dim)}
-  .rbar-track{flex:1;height:6px;border-radius:4px;background:rgba(255,255,255,.08);overflow:hidden}
-  .rbar-fill{display:block;height:100%}
-  .rbar-v{width:14px;text-align:right;color:var(--text-dim)}
-  .claim-name{font-weight:600;font-size:14px}
-  .claim-meta{color:var(--text-dim);font-size:11px;margin-top:2px}
+  .rbar-l{width:82px;color:var(--text-dim)}.rbar-track{flex:1;height:6px;border-radius:4px;background:rgba(255,255,255,.08);overflow:hidden}.rbar-fill{display:block;height:100%}.rbar-v{width:14px;text-align:right;color:var(--text-dim)}
+  .claim-name{font-weight:600;font-size:14px}.claim-meta{color:var(--text-dim);font-size:11px;margin-top:2px}
   .ctrls{display:flex;flex-direction:column;gap:8px}
+  .btn-go{background:#1f9d55;color:#fff;border:none;border-radius:8px;padding:8px 12px;font-weight:700;cursor:pointer;font-size:13px}
+  .btn-go:hover{background:#249a58}.btn-auto{opacity:.6;font-weight:400}
+  /* list + table */
+  .ltbl,.gtbl{width:100%;border-collapse:collapse;font-size:13px}
+  .ltbl td,.gtbl td,.gtbl th{padding:7px 9px;border-bottom:1px solid rgba(255,255,255,.07);vertical-align:middle}
+  .gtbl th{position:sticky;top:0;background:var(--panel,#161c2b);cursor:pointer;user-select:none;text-align:left;white-space:nowrap;color:var(--text-dim)}
+  .gtbl th:hover{color:#fff}
+  /* prep modal */
+  .backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:50}
+  .prep{background:#161c2b;border:1px solid rgba(255,255,255,.12);border-radius:14px;max-width:560px;width:92vw;max-height:88vh;overflow:auto;padding:18px 20px}
+  .prep h3{margin:0 0 2px}.prep .lane{margin:14px 0}.prep .lane h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em}
+  .kv{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:5px 8px;border-radius:7px;background:rgba(255,255,255,.04);margin:4px 0;font-size:13px}
+  .kv .k{color:var(--text-dim)}.kv .v{font-weight:600}
+  .kv button{background:#2d5bff;color:#fff;border:none;border-radius:6px;padding:3px 9px;cursor:pointer;font-size:11px}
+  .lane.green h4{color:#3fb950}.lane.amber h4{color:#d29922}.lane.red h4{color:#ff6b6b}
 </style>
 
 <section class="page-head">
   <h1>Settlement Claims</h1>
   <p class="subtle" style="margin:0;color:var(--text-dim);font-size:13px">
-    Auto-collected from the <strong>Mode Class Actions</strong> feed, ranked by <strong>priority</strong>
-    (value · urgency · ease · win-likelihood). Nothing is filed automatically.
-    Mark <em>Eligible</em> only for settlements you're truly a class member of — then openclaw prefills the
-    form and <strong>stops at the perjury attestation for you to submit</strong>.
-    <% if (source !== 'db') { %><span style="color:#e0a030">· showing parsed backfill (DB offline — mutations disabled)</span><% } %>
+    Ranked by <strong>priority</strong>. <strong>Claim it &amp; Go</strong> marks it eligible, prefills your
+    identity + payment (PayPal), opens the official claim portal, and <strong>stops at the perjury attestation
+    for you to submit</strong>. Nothing is filed automatically.
+    <% if (source !== 'db') { %><span style="color:#e0a030">· backfill view (DB offline)</span><% } %>
   </p>
+  <div class="views">
+    <button data-v="grid" class="on">🃏 Grid</button>
+    <button data-v="list">📋 List</button>
+    <button data-v="table">▦ Table</button>
+  </div>
 </section>
 
-<section>
-  <% if (!open.length) { %>
-    <p class="subtle" style="color:var(--text-dim)">No open claims.</p>
-  <% } %>
-  <% open.forEach(function(s){ var t = TIER[s.tier] || TIER.low; %>
+<section id="v-grid">
+  <% if (!open.length) { %><p class="subtle" style="color:var(--text-dim)">No open claims.</p><% } %>
+  <% open.forEach(function(s){ var t=TIER[s.tier]||TIER.low; %>
     <div class="claim-card" data-id="<%= s.id %>">
-      <div class="rank-badge">
-        <span class="n">#<%= s.rank %></span>
-        <span class="p"><%= s.priority %></span>
-      </div>
+      <div class="rank-badge"><span class="n">#<%= s.rank %></span><span class="p"><%= s.priority %></span></div>
       <div>
-        <div class="claim-name">
-          <% if (s.mode_url){ %><a href="<%= s.mode_url %>" target="_blank" rel="noopener noreferrer"><%= s.name %> ↗</a><% } else { %><%= s.name %><% } %>
-        </div>
-        <div class="claim-meta"><%= s.category||'' %> · seen <%= (s.first_seen_at||'').toString().slice(0,10) %></div>
+        <div class="claim-name"><% if (s.mode_url){ %><a href="<%= s.mode_url %>" target="_blank" rel="noopener noreferrer"><%= s.name %> ↗</a><% } else { %><%= s.name %><% } %></div>
+        <div class="claim-meta"><%= s.category||'' %></div>
         <div style="margin:8px 0">
           <span class="tier-chip" style="color:<%= t.color %>"><%= t.dot %> <%= t.label %></span>
           <span class="pill"><strong><%= money(s.payout_max_cents) %></strong></span>
@@ -75,59 +70,113 @@
         <% if (s.eligibility_question){ %><div class="claim-meta" style="max-width:520px"><%= s.eligibility_question %></div><% } %>
       </div>
       <div class="ctrls">
-        <div class="ratings">
-          <%- bar('value', s.ratings.value) %>
-          <%- bar('urgency', s.ratings.urgency) %>
-          <%- bar('ease', s.ratings.ease) %>
-          <%- bar('win likelihood', s.ratings.winlikelihood) %>
-        </div>
-        <div style="display:flex;gap:8px;align-items:center">
-          <% var es = s.eligibility_state||'unreviewed'; %>
-          <select class="elig" data-id="<%= s.id %>" style="background:transparent;color:inherit;border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:3px;flex:1">
-            <% ['unreviewed','eligible','maybe','not_eligible'].forEach(function(o){ %>
-              <option value="<%= o %>" <%= es===o?'selected':'' %>><%= o %></option>
-            <% }) %>
-          </select>
-          <% if (s.fill_state==='queued'){ %><span class="pill">queued</span>
-          <% } else if (s.fill_state==='prefilled_awaiting_submit'){ %><span class="pill due">review &amp; submit</span>
-          <% } else { %><button class="button fill" data-id="<%= s.id %>" <%= es==='eligible'?'':'disabled' %>>Prefill via openclaw</button><% } %>
-        </div>
+        <div class="ratings"><%- bar('value',s.ratings.value) %><%- bar('urgency',s.ratings.urgency) %><%- bar('ease',s.ratings.ease) %><%- bar('win likelihood',s.ratings.winlikelihood) %></div>
+        <% if (s.no_claim_required){ %><span class="pill">no form — automatic</span>
+        <% } else { %><button class="btn-go go" data-id="<%= s.id %>" data-name="<%= s.name %>">✅ Claim it &amp; Go</button><% } %>
       </div>
     </div>
   <% }) %>
 </section>
 
+<section id="v-list" style="display:none">
+  <table class="ltbl"><tbody>
+  <% open.forEach(function(s){ var t=TIER[s.tier]||TIER.low; %>
+    <tr>
+      <td style="width:34px;font-weight:700">#<%= s.rank %></td>
+      <td style="color:<%= t.color %>;width:16px"><%= t.dot %></td>
+      <td><%= s.name %></td>
+      <td style="width:80px"><strong><%= money(s.payout_max_cents) %></strong></td>
+      <td class="<%= s.days===0?'today':'' %>" style="width:110px;color:var(--text-dim)"><%= deadlinePill(s) %></td>
+      <td style="width:150px;text-align:right">
+        <% if (s.no_claim_required){ %><span class="pill">automatic</span><% } else { %><button class="btn-go go" data-id="<%= s.id %>" data-name="<%= s.name %>" style="padding:5px 10px">Go →</button><% } %>
+      </td>
+    </tr>
+  <% }) %>
+  </tbody></table>
+</section>
+
+<section id="v-table" style="display:none">
+  <table class="gtbl" id="gtbl">
+    <thead><tr>
+      <th data-k="rank" data-num>#</th><th data-k="priority" data-num>Prio</th>
+      <th data-k="rv" data-num>💰Val</th><th data-k="ru" data-num>⏰Urg</th><th data-k="re" data-num>⚡Ease</th><th data-k="rw" data-num>🎯Win</th>
+      <th data-k="payout" data-num>Payout</th><th data-k="days" data-num>Deadline</th><th data-k="proof">Proof</th><th data-k="name">Claim</th><th>Go</th>
+    </tr></thead>
+    <tbody>
+    <% open.forEach(function(s){ var t=TIER[s.tier]||TIER.low; %>
+      <tr data-rank="<%= s.rank %>" data-priority="<%= s.priority %>" data-rv="<%= s.ratings.value %>" data-ru="<%= s.ratings.urgency %>" data-re="<%= s.ratings.ease %>" data-rw="<%= s.ratings.winlikelihood %>" data-payout="<%= s.payout_max_cents||0 %>" data-days="<%= s.days==null?99999:s.days %>" data-proof="<%= proofLabel(s) %>" data-name="<%= s.name %>">
+        <td style="font-weight:700">#<%= s.rank %></td><td><%= s.priority %></td>
+        <td><%= s.ratings.value %></td><td><%= s.ratings.urgency %></td><td><%= s.ratings.ease %></td><td><%= s.ratings.winlikelihood %></td>
+        <td><strong><%= money(s.payout_max_cents) %></strong></td>
+        <td class="<%= s.days===0?'today':'' %>"><%= deadlinePill(s) %></td>
+        <td><%= proofLabel(s) %></td>
+        <td><%= s.name %></td>
+        <td><% if (s.no_claim_required){ %><span class="pill">auto</span><% } else { %><button class="btn-go go" data-id="<%= s.id %>" data-name="<%= s.name %>" style="padding:4px 9px">Go</button><% } %></td>
+      </tr>
+    <% }) %>
+    </tbody>
+  </table>
+</section>
+
 <% if (expired.length){ %>
 <section style="margin-top:26px">
   <h2 style="font-size:15px;color:var(--text-dim)">Recently expired <span class="pill"><%= expired.length %></span></h2>
-  <% expired.forEach(function(s){ var t = TIER[s.tier] || TIER.low; %>
-    <div class="claim-card expired" data-id="<%= s.id %>">
-      <div class="rank-badge"><span class="n">—</span><span class="p"><%= s.priority %></span></div>
-      <div>
-        <div class="claim-name">
-          <% if (s.mode_url){ %><a href="<%= s.mode_url %>" target="_blank" rel="noopener noreferrer"><%= s.name %> ↗</a><% } else { %><%= s.name %><% } %>
-        </div>
-        <div class="claim-meta"><%= s.category||'' %> · <%= money(s.payout_max_cents) %> · <%= deadlinePill(s) %></div>
-      </div>
-      <div class="ctrls"><span class="tier-chip" style="color:<%= t.color %>"><%= t.dot %> <%= t.label %></span></div>
-    </div>
-  <% }) %>
+  <% expired.forEach(function(s){ %><div class="claim-meta">· <%= s.name %> — <%= money(s.payout_max_cents) %> — <%= deadlinePill(s) %></div><% }) %>
 </section>
 <% } %>
 
+<!-- prep modal -->
+<div class="backdrop" id="prep-bd">
+  <div class="prep">
+    <h3 id="prep-title">Claim it &amp; Go</h3>
+    <p class="claim-meta" id="prep-note"></p>
+    <div class="lane green"><h4>🟢 We'll fill this — verify &amp; paste</h4><div id="prep-green"></div></div>
+    <div class="lane amber"><h4>🟡 Needs you (once)</h4>
+      <div class="claim-meta">If the form asks for a <strong>Claim ID / PIN</strong> it's on your mailed notice (many claims allow filing without it). Claim-specific facts (property address, dates) and any proof upload are entered by you on the portal.</div>
+    </div>
+    <div class="lane red"><h4>🔴 Locked — by law</h4>
+      <div class="claim-meta">The "under penalty of perjury I am a class member" attestation + Submit are <strong>yours</strong>. Nothing is submitted for you.</div>
+    </div>
+    <div style="display:flex;gap:10px;margin-top:16px">
+      <a id="prep-open" href="#" target="_blank" rel="noopener" class="btn-go" style="text-decoration:none;display:inline-block">Open claim portal ↗</a>
+      <button onclick="document.getElementById('prep-bd').style.display='none'" style="background:transparent;color:var(--text-dim);border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 14px;cursor:pointer">Close</button>
+    </div>
+  </div>
+</div>
+
 <script>
-document.querySelectorAll('.elig').forEach(function(sel){
-  sel.addEventListener('change', function(){
-    fetch('/api/settlements/'+this.dataset.id+'/eligibility',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({state:this.value})})
-      .then(r=>r.json()).then(()=>location.reload());
-  });
-});
-document.querySelectorAll('.fill').forEach(function(btn){
-  btn.addEventListener('click', function(){
-    if(!confirm('Stage an openclaw job to PREFILL this claim? It will stop before submitting.')) return;
-    fetch('/api/settlements/'+this.dataset.id+'/fill',{method:'POST'}).then(r=>r.json())
-      .then(j=>{ alert(j.note||j.error||'staged'); location.reload(); });
-  });
-});
+(function(){
+  // view toggle (persisted)
+  var views=['grid','list','table']; var cur=localStorage.setlView||'grid';
+  function show(v){ cur=v; localStorage.setlView=v; views.forEach(function(x){ document.getElementById('v-'+x).style.display = x===v?'':'none'; }); document.querySelectorAll('.views button').forEach(function(b){ b.classList.toggle('on', b.dataset.v===v); }); }
+  document.querySelectorAll('.views button').forEach(function(b){ b.onclick=function(){ show(b.dataset.v); }; });
+  show(cur);
+
+  // sortable table
+  var gt=document.getElementById('gtbl');
+  if(gt){ gt.querySelectorAll('th[data-k]').forEach(function(th){ var asc=true; th.onclick=function(){ var k=th.dataset.k,num=th.hasAttribute('data-num'); var tb=gt.tBodies[0]; var rows=[].slice.call(tb.rows);
+    rows.sort(function(a,b){ var x=a.dataset[k],y=b.dataset[k]; if(num){x=+x;y=+y;} else {x=(x||'').toLowerCase();y=(y||'').toLowerCase();} return (x<y?-1:x>y?1:0)*(asc?1:-1); });
+    asc=!asc; rows.forEach(function(r){ tb.appendChild(r); }); }; }); }
+
+  // Claim it & Go
+  function esc(s){ return (s||'').replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
+  function kv(k,v){ if(!v) return ''; return '<div class="kv"><span class="k">'+esc(k)+'</span><span class="v">'+esc(v)+'</span><button onclick="navigator.clipboard.writeText(this.previousSibling.textContent)">copy</button></div>'; }
+  document.querySelectorAll('.go').forEach(function(btn){ btn.onclick=function(){
+    var id=btn.dataset.id;
+    btn.disabled=true; btn.textContent='…';
+    fetch('/api/settlements/'+id+'/claim-and-go',{method:'POST'}).then(function(r){return r.json();}).then(function(j){
+      btn.disabled=false; btn.textContent='✅ Claim it & Go';
+      if(j.error){ alert(j.error); return; }
+      var f=j.fields||{};
+      document.getElementById('prep-title').textContent=btn.dataset.name||'Claim it & Go';
+      document.getElementById('prep-note').textContent=j.note||'';
+      document.getElementById('prep-green').innerHTML =
+        kv('First name',f.first_name)+kv('Last name',f.last_name)+kv('Street',f.street)+kv('City',f.city)+kv('State',f.state)+kv('Zip',f.zip)+kv('Email',f.email)+kv('Phone',f.phone)+kv('Payment',f.payment_method+' → '+f.paypal_email);
+      var open=document.getElementById('prep-open');
+      if(j.mode_url){ open.href=j.mode_url; open.style.display=''; window.open(j.mode_url,'_blank','noopener'); } else { open.style.display='none'; }
+      document.getElementById('prep-bd').style.display='flex';
+    }).catch(function(){ btn.disabled=false; btn.textContent='✅ Claim it & Go'; alert('error'); });
+  }; });
+})();
 </script>
 <%- include('partials/footer') %>

← f450f59 amazon-orders: 30-min Gmail poller (George bridge) → purchas  ·  back to AbramsOS  ·  settlements: remove duplicate claim-and-go (keep the payment ae4ebab →