← back to Gracie Internal
gracie-internal: add Memo/Stock/Price chip actions (shared vendor-requests module)
69f55eb593ace994bdf1d3f8088b8b34545416cf · 2026-07-15 15:01:21 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A data/inquiries.jsonlM public/index.htmlM server.js
Diff
commit 69f55eb593ace994bdf1d3f8088b8b34545416cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 15 15:01:21 2026 -0700
gracie-internal: add Memo/Stock/Price chip actions (shared vendor-requests module)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
data/inquiries.jsonl | 1 +
public/index.html | 40 ++++++++++++++++++++++++++++++++++++++++
server.js | 12 ++++++++++++
3 files changed, 53 insertions(+)
diff --git a/data/inquiries.jsonl b/data/inquiries.jsonl
new file mode 100644
index 0000000..2eda301
--- /dev/null
+++ b/data/inquiries.jsonl
@@ -0,0 +1 @@
+{"at":"2026-07-15T22:01:13.987Z","req_no":"REQ-260715-7","type":"memo","status":"open","sku":"TEST","qty":"","note":""}
diff --git a/public/index.html b/public/index.html
index 0a96f7b..567d1b9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -74,6 +74,18 @@
body.hide-tags .card .tags { display:none; }
body.hide-when .card .when { display:none; }
body.hide-ptn .card .ptn { display:none; }
+ /* purchasing actions on each card: Memo / Stock / Price → /api/request */
+ .card .acts { display:flex; gap:4px; padding:0 10px 10px; }
+ .card .acts .act { flex:1; font:inherit; font-size:10.5px; color:var(--mut); background:transparent; border:1px solid var(--stroke); border-radius:6px; padding:5px 2px; cursor:pointer; }
+ .card .acts .act:hover { border-color:var(--accent); color:var(--accent); }
+ .card .acts .act:disabled { opacity:.5; cursor:default; }
+ body.imgonly .card .acts { display:none; }
+ #toast { position:fixed; left:50%; bottom:20px; transform:translateX(-50%) translateY(20px); max-width:min(560px,92vw);
+ background:#fff; color:var(--ink); border:1px solid var(--stroke); border-radius:9px; padding:11px 17px; font-size:13px;
+ box-shadow:0 10px 34px rgba(28,26,23,.18); opacity:0; pointer-events:none; transition:.25s; z-index:60; }
+ #toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
+ #toast.ok { border-color:var(--accent); }
+ #toast.err { border-color:#c0392b; }
/* image-only mode = collapse the entire card body → pure texture wall */
body.imgonly .card .body { display:none; }
body.imgonly .grid { gap:6px; }
@@ -333,6 +345,9 @@ function render() {
const frag = document.createDocumentFragment();
for (const p of rows) {
const card = document.createElement('div'); card.className = 'card';
+ card.dataset.sku = p.dw_sku || '';
+ card.dataset.mfr = p.mfr_sku || '';
+ card.dataset.title = p.pattern_name || p.mfr_sku || '';
const img = p.image_url ? `<img loading="lazy" src="${esc(p.image_url)}?w=500&q=70" alt="${esc(p.pattern_name)}">` : '';
const flags = [];
if (p.settlement_flag && p.settlement_flag !== 'OK') flags.push(`<span class="flag gate">gate: ${esc(p.settlement_flag)}</span>`);
@@ -347,12 +362,37 @@ function render() {
<div class="flags">${flags.join('')}</div>
<div class="tags">${tags}</div>
<div class="when" title="${esc(p.created_at)}">🕓 ${fmtDate(p.created_at)}</div>
+ </div>
+ <div class="acts">
+ <button class="act" data-act="memo" title="Log a memo-sample request">Memo</button>
+ <button class="act" data-act="stock" title="Email the vendor a stock check">Stock</button>
+ <button class="act" data-act="price" title="Email the vendor for current price">Price</button>
</div>`;
frag.appendChild(card);
}
grid.appendChild(frag);
}
+/* ── Purchasing actions on each card: Memo / Stock / Price → /api/request ── */
+grid.addEventListener('click', async (e) => {
+ const btn = e.target.closest('.act'); if (!btn) return;
+ const card = btn.closest('.card'); const type = btn.dataset.act;
+ const payload = { type, dw_sku: card.dataset.sku, mfr_sku: card.dataset.mfr, title: card.dataset.title };
+ const old = btn.textContent; btn.disabled = true; btn.textContent = '…';
+ try {
+ const r = await fetch(location.origin + '/api/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then((r) => r.json());
+ toast(r.ok ? (r.message || ('Logged ' + (r.req_no || ''))) : (r.error || 'request failed'), !!r.ok);
+ } catch (err) { toast('request failed: ' + err.message, false); }
+ btn.disabled = false; btn.textContent = old;
+});
+let toastT = null;
+function toast(msg, ok) {
+ let t = document.getElementById('toast');
+ if (!t) { t = document.createElement('div'); t.id = 'toast'; document.body.appendChild(t); }
+ t.textContent = msg; t.className = 'show ' + (ok ? 'ok' : 'err');
+ clearTimeout(toastT); toastT = setTimeout(() => t.className = '', 6000);
+}
+
/* ── top-bar controls (search + sort + density + image-only, all persisted) ── */
document.getElementById('sort').onchange = (e) => { localStorage.gracieSort = e.target.value; render(); };
document.getElementById('density').oninput = (e) => { document.documentElement.style.setProperty('--cardmin', e.target.value + 'px'); localStorage.gracieDensity = e.target.value; };
diff --git a/server.js b/server.js
index 9ea6cd8..c6c2a7e 100644
--- a/server.js
+++ b/server.js
@@ -191,6 +191,18 @@ app.get('/api/facets', (_req, res) => res.json(facets()));
app.use(express.static(path.join(__dirname, 'public')));
+// ── Internal purchasing requests: Request Memo / Check Stock / Get Price ──────────
+// Shared module every internal vendor viewer uses (astek reference). Logs a REQ to
+// dw_unified.vendor_requests; stock/price auto-email the vendor when an email is on
+// file. Behind Basic Auth (the click is the approval). Gracie is a quote-only line
+// with no vendor email on file, so requests log + prompt to call.
+app.use(express.json());
+require('./lib/vendor-requests').mountVendorRequests(app, {
+ vendorCode: 'gracie',
+ getVendor: () => ({ name: 'Gracie' }),
+ dataDir: __dirname + '/data',
+});
+
load().then(() => {
app.listen(PORT, () => console.log(`Gracie internal viewer → http://127.0.0.1:${PORT} (source=${USE_JSONL ? 'jsonl' : 'pg'})`));
// hot-refresh every 10 min so new scrapes/enrichment appear without restart —
← 5866b3a auto-save: 2026-07-15T13:06:41 (1 files) — lib/
·
back to Gracie Internal
·
(newest)