← 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
M routes/settlements.jsM views/settlements.ejs
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 & 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 & 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 & 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 & Go</h3>
+ <p class="claim-meta" id="prep-note"></p>
+ <div class="lane green"><h4>🟢 We'll fill this — verify & 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{'&':'&','<':'<','>':'>','"':'"'}[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 →