[object Object]

← back to Prestige Car Wash

Growth admin: tab-aware sort control on all data grids (sort+density rule; dtd:A, contrarian-gated)

2c10e9878b4417fa373beca78dfec7e1d981aa8f · 2026-07-26 07:55:32 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 2c10e9878b4417fa373beca78dfec7e1d981aa8f
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sun Jul 26 07:55:32 2026 -0700

    Growth admin: tab-aware sort control on all data grids (sort+density rule; dtd:A, contrarian-gated)
---
 public/admin/index.html | 39 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/public/admin/index.html b/public/admin/index.html
index f1df69e..a7885af 100644
--- a/public/admin/index.html
+++ b/public/admin/index.html
@@ -89,6 +89,7 @@
 
   <div class="controls" id="ctl" style="margin-top:16px">
     <input type="search" id="q" placeholder="Search…">
+    <label>Sort <select id="sort"></select></label>
     <label>Density <input type="range" id="density" min="2" max="6" step="1"></label>
     <span id="count" style="color:var(--mut);font-size:13px;font-weight:600"></span>
   </div>
@@ -110,6 +111,7 @@ function setDensity(v){v=Number(v);document.documentElement.style.setProperty('-
   document.documentElement.style.setProperty('--fs',Math.max(.85,Math.min(1.2,1+(3-v)*0.05)).toFixed(3));}
 $('#density').oninput=e=>{setDensity(e.target.value);localStorage.setItem('pcwadmin.density',e.target.value)};
 let t;$('#q').oninput=()=>{clearTimeout(t);t=setTimeout(render,200)};
+$('#sort').onchange=e=>{localStorage.setItem('pcwadmin.sort.'+CUR,e.target.value);render()};
 
 // ---- tab definitions -------------------------------------------------------
 const TABS=[
@@ -166,7 +168,7 @@ let CMP={sel:null,plats:new Set(),media:[],tpl:{platforms:[],hashtag_sets:{},tem
 
 $('#tabs').innerHTML=TABS.map(t=>`<button data-id="${t.id}" class="${t.id===CUR?'on':''}" onclick="go('${t.id}')">${t.label}</button>`).join('');
 window.go=id=>{CUR=id;document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.id===id));
-  $('#q').value='';render();};
+  $('#q').value='';$('#sort').dataset.tab='';render();};
 
 function cardWrap(inner){return `<div class="card"><div class="body">${inner}</div></div>`;}
 // Expandable SWOT · costs · payback panel for an idea card. Renders only the
@@ -208,6 +210,31 @@ function analysisPanel(s){
   return `<details class="analysis"><summary>${summary}</summary><div class="analysis-body">${html}</div></details>`;
 }
 const gridWrap=cards=>`<div class="grid">${cards}</div>`;
+// --- tab-aware sort (dtd:A, 2026-07-26) — every api grid gets Newest/Oldest +
+// domain-specific keys + Title A–Z; selection persists per-tab in localStorage.
+const SORTKEYS={
+  suggestions:[{label:'Impact ↓',field:'impact',numeric:true,dir:-1},{label:'Effort ↑',field:'effort',numeric:true,dir:1},{label:'Priority ↓',calc:r=>(+r.impact||0)*2-(+r.effort||0),numeric:true,dir:-1}],
+  competitors:[{label:'Rating ↓',field:'rating',numeric:true,dir:-1},{label:'Reviews ↓',field:'reviews',numeric:true,dir:-1}],
+  services:[{label:'Price ↑',field:'price',numeric:true,dir:1},{label:'Price ↓',field:'price',numeric:true,dir:-1}],
+  ads:[{label:'Budget ↓',field:'budget_month',numeric:true,dir:-1}],
+  holidays:[{label:'Date ↑',field:'date',dir:1}],
+  directories:[{label:'Priority',field:'priority',dir:1}]
+};
+const TITLEFIELD={suggestions:'title',competitors:'name',services:'name',ads:'headline',holidays:'name',directories:'name',leads:'name'};
+function sortOptsFor(id){
+  const opts=[{label:'Newest',field:'created_at',dir:-1},{label:'Oldest',field:'created_at',dir:1},...(SORTKEYS[id]||[])];
+  if(TITLEFIELD[id]) opts.push({label:'Title A–Z',field:TITLEFIELD[id],dir:1});
+  return opts;
+}
+function cmpBy(opt){
+  const get=opt.calc?opt.calc:(r=>r[opt.field]);
+  return (a,b)=>{
+    let x=get(a),y=get(b);
+    if(opt.numeric){x=Number(x)||0;y=Number(y)||0;return (x-y)*opt.dir;}
+    x=String(x==null?'':x).toLowerCase();y=String(y==null?'':y).toLowerCase();
+    return (x<y?-1:x>y?1:0)*opt.dir;
+  };
+}
 
 function render(){
   const tab=TABS.find(t=>t.id===CUR);
@@ -224,6 +251,16 @@ function render(){
     const q=$('#q').value.trim().toLowerCase();
     let list=rows;
     if(q&&tab.search) list=rows.filter(r=>tab.search.some(f=>String(r[f]||'').toLowerCase().includes(q)));
+    // tab-aware sort: (re)build the sort select on tab change, then apply the chosen key
+    const opts=sortOptsFor(tab.id), sortEl=$('#sort');
+    if(sortEl.dataset.tab!==tab.id){
+      sortEl.innerHTML=opts.map((o,i)=>`<option value="${i}">${enc(o.label)}</option>`).join('');
+      const saved=localStorage.getItem('pcwadmin.sort.'+tab.id);
+      sortEl.value=(saved!=null&&Number(saved)<opts.length)?saved:'0';
+      sortEl.dataset.tab=tab.id;
+    }
+    const opt=opts[Number(sortEl.value)||0]||opts[0];
+    list=[...list].sort(cmpBy(opt));
     $('#count').textContent=list.length+' item'+(list.length!==1?'s':'');
     $('#view').innerHTML=list.length?gridWrap(list.map(tab.card).join('')):'<p style="color:var(--mut)">Nothing here yet.</p>';
   });

← 6edbd5d auto-save: 2026-07-26T07:41:30 (2 files) — public/admin/inde  ·  back to Prestige Car Wash  ·  Growth admin: per-idea Action Brief (printable/copyable), dt 060a5b2 →