← back to AbramsOS
tick 7: /claims dashboard UI + claim detail with draft preview
1f60c67b5cbdc21f4262220ca0dd1cb1d9e4f955 · 2026-05-10 00:53:39 -0700 · Steve
- views/claims.ejs: card grid showing routing/state/due-date per claim
- views/claim-detail.ejs: full draft letter (Georgia serif), evidence index,
pending action_queue rows with 'Approve draft' button
- routes/claims.js: GET /claims (list HTML), GET /claims/:id (detail HTML)
- nav: /claims link added
- Approving = 'I've reviewed this'; send wire-up still deferred to George tick
Files touched
M routes/claims.jsA views/claim-detail.ejsA views/claims.ejsM views/partials/header.ejs
Diff
commit 1f60c67b5cbdc21f4262220ca0dd1cb1d9e4f955
Author: Steve <steve@designerwallcoverings.com>
Date: Sun May 10 00:53:39 2026 -0700
tick 7: /claims dashboard UI + claim detail with draft preview
- views/claims.ejs: card grid showing routing/state/due-date per claim
- views/claim-detail.ejs: full draft letter (Georgia serif), evidence index,
pending action_queue rows with 'Approve draft' button
- routes/claims.js: GET /claims (list HTML), GET /claims/:id (detail HTML)
- nav: /claims link added
- Approving = 'I've reviewed this'; send wire-up still deferred to George tick
---
routes/claims.js | 18 ++++++++++++
views/claim-detail.ejs | 75 +++++++++++++++++++++++++++++++++++++++++++++++
views/claims.ejs | 37 +++++++++++++++++++++++
views/partials/header.ejs | 1 +
4 files changed, 131 insertions(+)
diff --git a/routes/claims.js b/routes/claims.js
index 0322945..be584cf 100644
--- a/routes/claims.js
+++ b/routes/claims.js
@@ -6,6 +6,24 @@ const strategist = require('../lib/claim-strategist');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
+// HTML — list view
+router.get('/claims', async (_req, res) => {
+ const r = await db.query(
+ `SELECT id, claim_type, routing, state, draft_subject, due_at, created_at, asset_table, asset_id
+ FROM claim_case WHERE user_id = $1 ORDER BY created_at DESC LIMIT 200`,
+ [DEV_USER_ID]
+ );
+ res.render('claims', { claims: r.rows });
+});
+
+// HTML — detail view
+router.get('/claims/:id', async (req, res) => {
+ const c = await db.query(`SELECT * FROM claim_case WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
+ if (!c.rows.length) return res.status(404).render('error', { error: 'claim not found' });
+ const a = await db.query(`SELECT * FROM action_queue WHERE case_id = $1 ORDER BY created_at`, [req.params.id]);
+ res.render('claim-detail', { claim: c.rows[0], actions: a.rows });
+});
+
router.get('/api/claims', async (_req, res) => {
const r = await db.query(
`SELECT id, claim_type, routing, state, draft_subject, due_at, created_at, asset_table, asset_id
diff --git a/views/claim-detail.ejs b/views/claim-detail.ejs
new file mode 100644
index 0000000..0cf43c6
--- /dev/null
+++ b/views/claim-detail.ejs
@@ -0,0 +1,75 @@
+<%- include('partials/header', { title: 'Claim · ' + (claim.draft_subject || claim.id) }) %>
+
+<section class="page-head">
+ <a class="link" href="/claims" style="color:var(--text-dim);font-size:13px">← All claims</a>
+ <h1 style="margin-top:8px"><%= claim.draft_subject || (claim.claim_type + ' request') %></h1>
+ <span class="status <%= claim.state === 'sent' ? 'connected' : claim.state === 'draft' ? 'syncing' : 'not_connected' %>" style="font-size:12px;padding:4px 10px;border-radius:999px;align-self:flex-start"><%= claim.state %></span>
+</section>
+
+<section class="glass" style="padding:22px 24px">
+ <dl class="meta" style="display:grid;grid-template-columns:max-content 1fr;gap:6px 16px;margin:0 0 16px 0">
+ <dt style="color:var(--text-dim);font-size:12px">Routing</dt><dd style="margin:0"><%= claim.routing %></dd>
+ <dt style="color:var(--text-dim);font-size:12px">Claim type</dt><dd style="margin:0"><%= claim.claim_type %></dd>
+ <dt style="color:var(--text-dim);font-size:12px">Desired remedy</dt><dd style="margin:0"><%= claim.desired_remedy || '—' %></dd>
+ <% if (claim.due_at) { %>
+ <dt style="color:var(--text-dim);font-size:12px">Due by</dt><dd style="margin:0"><%= new Date(claim.due_at).toLocaleString() %></dd>
+ <% } %>
+ <dt style="color:var(--text-dim);font-size:12px">Source</dt><dd style="margin:0"><%= claim.metadata_jsonb && claim.metadata_jsonb.draft_source || 'template' %></dd>
+ </dl>
+</section>
+
+<section class="glass" style="padding:22px 24px">
+ <h2 style="margin:0 0 14px 0;font-size:16px">Draft letter</h2>
+ <pre style="white-space:pre-wrap;font-family:Georgia,'Times New Roman',serif;font-size:14px;line-height:1.55;color:var(--text);background:rgba(0,0,0,0.18);padding:18px;border-radius:10px;border:1px solid var(--border)"><%= claim.draft_body %></pre>
+</section>
+
+<% if (claim.evidence_jsonb && claim.evidence_jsonb.length) { %>
+ <section class="glass" style="padding:22px 24px">
+ <h2 style="margin:0 0 12px 0;font-size:16px">Evidence index</h2>
+ <ul style="margin:0;padding-left:18px">
+ <% claim.evidence_jsonb.forEach(e => { %>
+ <li style="font-size:13px;color:var(--text-dim);margin-bottom:4px"><%= e.kind %>: <%= e.id %></li>
+ <% }) %>
+ </ul>
+ </section>
+<% } %>
+
+<section class="glass" style="padding:22px 24px">
+ <h2 style="margin:0 0 12px 0;font-size:16px">Pending actions</h2>
+ <% if (!actions.length) { %>
+ <p class="subtle" style="color:var(--text-dim);margin:0">No actions queued.</p>
+ <% } else { %>
+ <% actions.forEach(a => { %>
+ <article style="padding:12px 14px;border:1px solid var(--border);border-radius:10px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;gap:12px">
+ <div>
+ <div style="font-weight:500"><%= a.action_type %></div>
+ <div style="font-size:12px;color:var(--text-dim)">approval: <%= a.approval_level %> · state: <%= a.state %></div>
+ </div>
+ <% if (a.state === 'pending') { %>
+ <div style="display:flex;gap:8px">
+ <button class="button primary" data-approve="<%= a.id %>">Approve draft</button>
+ </div>
+ <% } else if (a.state === 'approved') { %>
+ <span class="status connected" style="font-size:11px;padding:3px 9px;border-radius:999px">approved · awaiting send wire-up</span>
+ <% } else { %>
+ <span class="subtle" style="font-size:12px;color:var(--text-dim)"><%= a.state %></span>
+ <% } %>
+ </article>
+ <% }) %>
+ <% } %>
+ <p class="subtle" style="margin-top:14px;font-size:11px;color:var(--text-dim)">Approving here does NOT send. The send wire-up to George email is a future tick. Until then, approve = "I've reviewed this draft and would send it."</p>
+</section>
+
+<script>
+document.querySelectorAll('[data-approve]').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ btn.disabled = true;
+ btn.textContent = 'Approving…';
+ const r = await fetch('/api/claims/<%= claim.id %>/actions/' + btn.dataset.approve + '/approve', { method: 'POST' });
+ if (r.ok) { btn.textContent = 'Approved ✓'; setTimeout(() => location.reload(), 800); }
+ else { btn.textContent = 'Failed'; btn.disabled = false; }
+ });
+});
+</script>
+
+<%- include('partials/footer') %>
diff --git a/views/claims.ejs b/views/claims.ejs
new file mode 100644
index 0000000..4de608f
--- /dev/null
+++ b/views/claims.ejs
@@ -0,0 +1,37 @@
+<%- include('partials/header', { title: 'Claims' }) %>
+
+<section class="page-head">
+ <h1>Claims</h1>
+ <p class="subtle" style="margin:0;color:var(--text-dim);font-size:13px">Drafts only. Nothing leaves AbramsOS without your explicit approval per action.</p>
+</section>
+
+<% if (!claims.length) { %>
+ <section class="empty glass">
+ <p>No claims yet. Claims are created automatically when a reminder fires (returns window closing, warranty expiry, recall match).</p>
+ <a class="button" href="/import">Go to Import</a>
+ </section>
+<% } else { %>
+ <section class="connector-grid">
+ <% claims.forEach(c => { %>
+ <a class="connector-card glass" href="/claims/<%= c.id %>" style="text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:10px">
+ <header>
+ <span class="name">
+ <span class="icon"><%= c.routing[0].toUpperCase() %></span>
+ <%= c.draft_subject || (c.claim_type + ' — ' + c.routing) %>
+ </span>
+ <span class="status <%= c.state === 'sent' ? 'connected' : c.state === 'draft' ? 'syncing' : 'not_connected' %>">
+ <%= c.state %>
+ </span>
+ </header>
+ <p class="desc">
+ <strong><%= c.claim_type %></strong> →
+ <%= c.routing %>
+ <% if (c.due_at) { %> · due <%= new Date(c.due_at).toLocaleDateString() %><% } %>
+ </p>
+ <p class="subtle" style="font-size:11px;color:var(--text-dim)">created <%= new Date(c.created_at).toLocaleString() %></p>
+ </a>
+ <% }) %>
+ </section>
+<% } %>
+
+<%- include('partials/footer') %>
diff --git a/views/partials/header.ejs b/views/partials/header.ejs
index 15e94dd..323dcd8 100644
--- a/views/partials/header.ejs
+++ b/views/partials/header.ejs
@@ -25,6 +25,7 @@
<a href="/import">Import</a>
<a href="/connectors">Connectors</a>
<a href="/purchases">Purchases</a>
+ <a href="/claims">Claims</a>
</nav>
<% if (typeof userId !== 'undefined' && userId) { %>
<form method="post" action="/signout" style="margin:0">
← 7294ca3 tick 6: claim_case + action_queue + claim strategist (drafts
·
back to AbramsOS
·
tick 8: CSRF protection on HTML POST forms 1f31901 →