[object Object]

← back to Interiordesignershowroom

admin: brand-level line selector — hide/show a whole brand across every advertiser

dcba584c958c3cf8f0edf3126210926a04502ad6 · 2026-08-03 09:10:11 -0700 · Steve Abrams

Files touched

Diff

commit dcba584c958c3cf8f0edf3126210926a04502ad6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 09:10:11 2026 -0700

    admin: brand-level line selector — hide/show a whole brand across every advertiser
---
 routes/admin.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 105 insertions(+), 1 deletion(-)

diff --git a/routes/admin.js b/routes/admin.js
index 5348807..41919b6 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -127,7 +127,7 @@ a.ezjoin:hover{background:#6f5c40}
 .why{color:#6b6259;font-size:.82rem}
 </style></head><body>
 <header><h1>Interior Designer’s Showroom — Admin</h1>
-<nav><a href="/admin">Dashboard</a> &nbsp;·&nbsp; <a href="/admin/affiliates">Affiliates</a> &nbsp;·&nbsp; <a href="/">View site ↗</a></nav></header>
+<nav><a href="/admin">Dashboard</a> &nbsp;·&nbsp; <a href="/admin/brands">Brands</a> &nbsp;·&nbsp; <a href="/admin/affiliates">Affiliates</a> &nbsp;·&nbsp; <a href="/">View site ↗</a></nav></header>
 <main>${body}</main></body></html>`;
 
 router.get('/admin', async (req, res, next) => {
@@ -138,6 +138,8 @@ router.get('/admin', async (req, res, next) => {
         (SELECT count(*) FROM products) AS products,
         (SELECT count(*) FROM products WHERE featured) AS featured,
         (SELECT count(*) FROM products WHERE suppressed) AS suppressed,
+        (SELECT count(DISTINCT brand) FROM products WHERE brand IS NOT NULL AND brand <> '') AS brands,
+        (SELECT count(DISTINCT brand) FROM products WHERE suppressed AND brand IS NOT NULL AND brand <> '') AS brands_hidden,
         (SELECT count(*) FROM guides WHERE published) AS guides,
         (SELECT count(*) FROM clicks) AS clicks,
         (SELECT count(*) FROM clicks WHERE clicked_at > now() - interval '7 days') AS clicks7,
@@ -155,6 +157,7 @@ router.get('/admin', async (req, res, next) => {
       <div class="stat"><b>${c.products}</b><span>Products</span></div>
       <div class="stat"><b>${c.featured}</b><span>Featured</span></div>
       <div class="stat"><b>${c.suppressed}</b><span>Suppressed${Number(c.suppressed) ? ' · hidden' : ''}</span></div>
+      <a class="stat" href="/admin/brands" style="text-decoration:none;color:inherit"><b>${c.brands}</b><span>Brands${Number(c.brands_hidden) ? ` · <span style="color:#b1483c">${c.brands_hidden} hidden</span>` : ' →'}</span></a>
       <div class="stat"><b>${c.guides}</b><span>Published guides</span></div>
       <div class="stat"><b>${c.clicks}</b><span>Total clicks</span></div>
       <div class="stat"><b>${c.clicks7}</b><span>Clicks · 7 days</span></div>
@@ -218,6 +221,107 @@ router.post('/admin/guides/:id/published', async (req, res, next) => {
   catch (e) { next(e); }
 });
 
+// --- Brands: select a whole BRAND LINE and hide/show it across every advertiser --
+// The fourth join key. network/advertiser toggles (affiliate_settings) hide by SOURCE;
+// the per-product Hide flips ONE row. A brand, though, can be carried by several
+// advertisers at once — e.g. "Honiture" sold by the Honiture merchant AND by resellers
+// (AUKRO, BSC). Killing the merchant leaves the reseller-carried items live. This page
+// flips `products.suppressed` for EVERY row of a brand in one click, so a cross-advertiser
+// line is switched off (or back on) as a unit. Reversible; nothing is deleted.
+function brandBtn(brand, action, label, disabled) {
+  if (disabled) return `<button type="button" disabled style="opacity:.4;cursor:not-allowed">${label}</button>`;
+  return `<form method="post" action="/admin/brands/suppress" style="display:inline;margin:0 3px 0 0">
+    <input type="hidden" name="brand" value="${esc(brand)}">
+    <input type="hidden" name="action" value="${action}">
+    <button>${label}</button></form>`;
+}
+
+router.get('/admin/brands', async (req, res, next) => {
+  try {
+    const q = req.query.q ? String(req.query.q).slice(0, 60) : '';
+    const params = [];
+    let filter = `brand IS NOT NULL AND brand <> ''`;
+    if (q) { params.push('%' + q + '%'); filter += ` AND brand ILIKE $${params.length}`; }
+    const { rows } = await db.query(`SELECT brand,
+        count(*) AS products,
+        count(*) FILTER (WHERE in_stock) AS in_stock,
+        count(*) FILTER (WHERE suppressed) AS suppressed,
+        count(DISTINCT advertiser) AS advertisers,
+        string_agg(DISTINCT advertiser, ', ' ORDER BY advertiser) AS advertiser_list,
+        max(created_at) AS latest
+      FROM products WHERE ${filter}
+      GROUP BY brand
+      ORDER BY (count(*) FILTER (WHERE suppressed) > 0) DESC, count(*) DESC
+      LIMIT 300`, params);
+
+    const onOff = (on) => `<span class="pill ${on ? 'on' : 'off'}">${on ? '● ON' : '○ OFF'}</span>`;
+    const hiddenBrands = rows.filter((r) => Number(r.suppressed) > 0).length;
+
+    const brandRows = rows.map((r) => {
+      const total = Number(r.products), hid = Number(r.suppressed), advs = Number(r.advertisers);
+      const allHidden = hid === total, allVisible = hid === 0;
+      // status pill: fully visible / partially hidden / fully hidden
+      const status = allVisible ? onOff(true)
+        : allHidden ? onOff(false)
+        : `<span class="pill" style="background:#faf1e0;border-color:#e5cf9e;color:#9a6a1c">◐ ${hid}/${total} hidden</span>`;
+      // cross-advertiser badge — the Honiture case: one brand, many merchants
+      const spread = advs > 1
+        ? `<span class="pill" title="${esc(r.advertiser_list || '')}" style="background:#eef2f7;border-color:#c6d3e2;color:#3a5578;margin-left:6px">spans ${advs} advertisers</span>`
+        : '';
+      // action buttons: Hide all (disabled if already fully hidden), Unhide all (disabled if none hidden)
+      const actions = brandBtn(r.brand, 'hide', 'Hide brand', allHidden)
+        + brandBtn(r.brand, 'unhide', 'Unhide brand', allVisible);
+      return `<tr class="${allHidden ? 'row-off' : ''}">
+        <td><a href="/shop?q=${encodeURIComponent(r.brand)}" target="_blank" rel="noopener" style="color:var(--ink);font-weight:600;text-decoration:none">${esc(r.brand)}</a> ${spread}</td>
+        <td>${advs}${r.advertiser_list ? ` <span class="subtle" title="${esc(r.advertiser_list)}">›</span>` : ''}</td>
+        <td>${r.in_stock} <span class="subtle">/ ${total}</span></td>
+        <td>${hid ? `<b style="color:#b1483c">${hid}</b>` : '0'}</td>
+        <td>${when(r.latest)}</td>
+        <td>${status}</td>
+        <td style="white-space:nowrap">${actions}</td>
+      </tr>`;
+    }).join('');
+
+    const intro = `<p class="subtle" style="margin:0 0 10px">
+      Select a whole <b>brand line</b> and switch it off across <i>every</i> advertiser at once —
+      the reversible "check it off as no" at the brand level. Hiding a brand flips it out of the
+      storefront, room builder, and brand filters everywhere it appears; it stays in the database and
+      returns the moment you un-hide it. Use this for a brand carried by several merchants (a
+      <b>spans N advertisers</b> badge marks those) where a single advertiser switch wouldn't catch it all.
+      ${hiddenBrands ? `<b style="color:#b1483c"> ${hiddenBrands} brand${hiddenBrands === 1 ? '' : 's'} currently hidden (fully or partly).</b>` : ''}</p>`;
+
+    const searchForm = `<form method="get" action="/admin/brands" style="margin:0 0 14px">
+      <input name="q" value="${esc(q)}" placeholder="Search brands…" style="padding:6px 10px;border:1px solid var(--line);border-radius:5px;font-size:.85rem;min-width:240px">
+      <button style="padding:6px 12px">Search</button>
+      ${q ? ` <a href="/admin/brands" class="subtle" style="margin-left:6px">clear</a>` : ''}</form>`;
+
+    res.send(shell(`
+      <h1 style="font-size:1.15rem;margin:0 0 4px">Brands — hide or show a whole line</h1>
+      ${intro}
+      ${searchForm}
+      <table>
+        <tr><th>Brand</th><th>Advertisers</th><th>In-stock / total</th><th>Hidden</th><th>Newest item</th><th>Status</th><th>Action</th></tr>
+        ${brandRows || `<tr><td colspan="7" class="subtle">No brands${q ? ` matching “${esc(q)}”` : ''}.</td></tr>`}
+      </table>
+      ${rows.length >= 300 ? `<p class="subtle">Showing the first 300 brands — narrow with search.</p>` : ''}
+    `));
+  } catch (e) { next(e); }
+});
+
+// Flip `suppressed` for EVERY product of one brand. action=hide → TRUE, unhide → FALSE.
+// Guarded: the brand must actually exist in the catalog. Reversible, kept in DB.
+router.post('/admin/brands/suppress', async (req, res, next) => {
+  try {
+    const brand = String(req.body.brand || '').trim().slice(0, 200);
+    const hide = req.body.action !== 'unhide'; // default = hide
+    if (!brand) return res.status(400).send('Missing brand.');
+    const exists = await db.query('SELECT 1 FROM products WHERE brand=$1 LIMIT 1', [brand]);
+    if (!exists.rowCount) return res.status(404).send('Unknown brand.');
+    await db.query('UPDATE products SET suppressed=$2, updated_at=now() WHERE brand=$1', [brand, hide]);
+    res.redirect('/admin/brands');
+  } catch (e) { next(e); }
+});
+
 // --- Affiliates: see every link source & switch it on/off -----------------
 // An affiliate is either a whole NETWORK (cj/amazon/...) or a specific ADVERTISER
 // (merchant) on that network. OFF here => that source's products vanish from the

← 99f7508 auto-save: 2026-08-03T08:52:19 (7 files) — db/schema.sql lib  ·  back to Interiordesignershowroom  ·  admin: keyword line-hider with preview — catch a line across 770915b →