[object Object]

← back to Prestige Car Wash

Growth admin: per-idea Action Brief (printable/copyable), dtd:A, contrarian-gated (data-id anti-injection + go() closes modal)

060a5b29dbd348a21fcf661496bf0aa819208d72 · 2026-07-26 08:08:03 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 060a5b29dbd348a21fcf661496bf0aa819208d72
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sun Jul 26 08:08:03 2026 -0700

    Growth admin: per-idea Action Brief (printable/copyable), dtd:A, contrarian-gated (data-id anti-injection + go() closes modal)
---
 public/admin/index.html | 81 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 79 insertions(+), 2 deletions(-)

diff --git a/public/admin/index.html b/public/admin/index.html
index a7885af..ee474aa 100644
--- a/public/admin/index.html
+++ b/public/admin/index.html
@@ -73,6 +73,26 @@
   .cost-card.model{border-color:var(--brand);border-left-width:3px}
   .cost-card.model h5{text-transform:none}
   @media(max-width:560px){.swot-grid{grid-template-columns:1fr}}
+  /* --- Action Brief button + modal (dtd:A, 2026-07-26) --- */
+  .brief-btn{margin-top:10px;background:var(--panel);border:1px solid var(--line);color:var(--brand);border-radius:8px;padding:6px 11px;font-size:12px;font-weight:700;cursor:pointer}
+  .brief-btn:hover{border-color:var(--brand)}
+  #brief-modal{display:none;position:fixed;inset:0;z-index:100}
+  #brief-modal.open{display:block}
+  #brief-modal .backdrop{position:absolute;inset:0;background:rgba(0,0,0,.6)}
+  #brief-modal .sheet{position:relative;max-width:720px;margin:5vh auto;max-height:90vh;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:24px 26px}
+  #brief-modal h2{margin:0 0 4px;font-size:22px}
+  #brief-modal .meta{color:var(--mut);font-size:12.5px;margin-bottom:14px}
+  #brief-modal .sec{margin:14px 0}
+  #brief-modal .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--brand)}
+  #brief-modal .sec ul{margin:4px 0;padding-left:18px} #brief-modal .sec li{margin:3px 0;line-height:1.4}
+  .brief-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:20px;padding-top:14px;border-top:1px solid var(--line)}
+  @media print{
+    body *{visibility:hidden}
+    #brief-modal,#brief-modal *{visibility:visible}
+    #brief-modal{position:absolute;inset:0;display:block}
+    #brief-modal .backdrop,.brief-actions{display:none}
+    #brief-modal .sheet{margin:0;max-height:none;border:0;background:#fff;color:#000}
+  }
 </style>
 </head>
 <body>
@@ -97,6 +117,18 @@
   <div id="view" style="padding:8px 0 60px"></div>
 </div>
 
+<div id="brief-modal" role="dialog" aria-modal="true" aria-label="Action brief">
+  <div class="backdrop" onclick="closeBrief()"></div>
+  <div class="sheet">
+    <div id="brief-body"></div>
+    <div class="brief-actions">
+      <button class="btn" onclick="window.print()">🖨️ Print</button>
+      <button class="btn" onclick="copyBrief()">📋 Copy</button>
+      <button class="btn" onclick="closeBrief()">Close</button>
+    </div>
+  </div>
+</div>
+
 <script>
 const $=s=>document.querySelector(s);
 function fmtDate(iso){ if(!iso) return '—'; const d=new Date(iso); if(isNaN(d)) return '—';
@@ -141,7 +173,8 @@ const TABS=[
   {id:'suggestions',label:'💡 Grow the Business',api:'/api/admin/suggestions',search:['title','rationale','category'],
    card:s=>cardWrap(`<div class="row"><div class="cat">${enc(s.category)}</div>
        <span class="badge ${s.impact>=5?'b-good':s.impact>=3?'b-warn':'b-mut'}">impact ${s.impact} · effort ${s.effort}</span></div>
-     <div class="ttl">${enc(s.title)}</div><div class="blurb">${enc(s.rationale)}</div>${analysisPanel(s)}${when(s.created_at)}`)},
+     <div class="ttl">${enc(s.title)}</div><div class="blurb">${enc(s.rationale)}</div>${analysisPanel(s)}
+     <button class="brief-btn" data-id="${enc(s.id)}" onclick="openBrief(this.dataset.id)">📋 Action Brief</button>${when(s.created_at)}`)},
   {id:'ads',label:'📣 Online Ads',api:'/api/admin/ads',search:['platform','type','headline'],
    card:a=>cardWrap(`<div class="row"><div class="cat">${enc(a.platform)} · ${enc(a.type)}</div><span class="badge b-warn">${enc(a.status)}</span></div>
      <div class="ttl">${enc(a.headline)}</div><div class="blurb">${enc(a.body)}</div>
@@ -167,7 +200,7 @@ let CUR='overview';
 let CMP={sel:null,plats:new Set(),media:[],tpl:{platforms:[],hashtag_sets:{},templates:[]},bt:{},services:[]};
 
 $('#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));
+window.go=id=>{closeBrief();CUR=id;document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.id===id));
   $('#q').value='';$('#sort').dataset.tab='';render();};
 
 function cardWrap(inner){return `<div class="card"><div class="body">${inner}</div></div>`;}
@@ -235,6 +268,49 @@ function cmpBy(opt){
     return (x<y?-1:x>y?1:0)*opt.dir;
   };
 }
+// --- Action Brief (dtd:A, 2026-07-26): one-page printable/copyable idea brief ---
+let ROWCACHE={};      // last-fetched rows per tab, so the brief can look an idea up
+let BRIEFCUR=null;
+function briefHTML(s){
+  const pri=(+s.impact||0)*2-(+s.effort||0);
+  let h=`<h2>${enc(s.title)}</h2><div class="meta">${enc(s.category||'')} · Impact ${enc(s.impact)} · Effort ${enc(s.effort)} · Priority ${pri}${s.created_at?' · '+fmtDate(s.created_at):''}</div>`;
+  h+=`<div class="sec"><h4>Rationale</h4><p>${enc(s.rationale||'')}</p></div>`;
+  const w=s.swot;
+  if(w){const col=(t,a)=>Array.isArray(a)&&a.length?`<div class="sec"><h4>${t}</h4><ul>${a.map(x=>`<li>${enc(x)}</li>`).join('')}</ul></div>`:'';
+    h+=col('Strengths',w.strengths)+col('Weaknesses',w.weaknesses)+col('Opportunities',w.opportunities)+col('Threats',w.threats);}
+  const c=s.costs;
+  if(c){
+    if(c.note) h+=`<div class="sec"><h4>Costs — note</h4><p>${enc(c.note)}</p></div>`;
+    for(const [k,v] of Object.entries(c)){
+      if(k==='note'||!v||typeof v!=='object') continue;
+      const rows=Object.entries(v).filter(([kk,vv])=>kk!=='recommended'&&typeof vv!=='object').map(([kk,vv])=>`<li><b>${enc(kk.replace(/_/g,' '))}:</b> ${enc(vv)}</li>`).join('');
+      h+=`<div class="sec"><h4>${enc(k.replace(/_/g,' '))}</h4><ul>${rows}</ul></div>`;
+    }
+  }
+  return h;
+}
+function briefText(s){
+  const pri=(+s.impact||0)*2-(+s.effort||0);
+  let t=`${s.title}\n${s.category||''} · Impact ${s.impact} · Effort ${s.effort} · Priority ${pri}\n\nRATIONALE\n${s.rationale||''}\n`;
+  const w=s.swot;
+  if(w){const sec=(n,a)=>Array.isArray(a)&&a.length?`\n${n.toUpperCase()}\n`+a.map(x=>'- '+x).join('\n')+'\n':'';
+    t+=sec('Strengths',w.strengths)+sec('Weaknesses',w.weaknesses)+sec('Opportunities',w.opportunities)+sec('Threats',w.threats);}
+  const c=s.costs;
+  if(c){for(const [k,v] of Object.entries(c)){if(k==='note'||!v||typeof v!=='object')continue;
+    t+=`\n${k.replace(/_/g,' ').toUpperCase()}\n`+Object.entries(v).filter(([kk,vv])=>kk!=='recommended'&&typeof vv!=='object').map(([kk,vv])=>`- ${kk.replace(/_/g,' ')}: ${vv}`).join('\n')+'\n';}}
+  return t.trim();
+}
+window.openBrief=function(id){
+  const s=(ROWCACHE.suggestions||[]).find(x=>x.id===id); if(!s) return;
+  BRIEFCUR=s; document.getElementById('brief-body').innerHTML=briefHTML(s);
+  document.getElementById('brief-modal').classList.add('open');
+};
+window.closeBrief=function(){document.getElementById('brief-modal').classList.remove('open');};
+function briefToast(){const b=document.querySelector('.brief-actions button:nth-child(2)'); if(b){const o=b.textContent;b.textContent='✓ Copied';setTimeout(()=>{b.textContent=o;},1500);}}
+function fallbackCopy(txt){const ta=document.createElement('textarea');ta.value=txt;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');briefToast();}catch(e){}document.body.removeChild(ta);}
+window.copyBrief=function(){if(!BRIEFCUR)return;const txt=briefText(BRIEFCUR);
+  if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).then(briefToast).catch(()=>fallbackCopy(txt));}else fallbackCopy(txt);};
+document.addEventListener('keydown',e=>{if(e.key==='Escape')closeBrief();});
 
 function render(){
   const tab=TABS.find(t=>t.id===CUR);
@@ -248,6 +324,7 @@ function render(){
   if(tab.id==='credentials') return renderCreds();
   // generic data bucket
   fetch(tab.api).then(r=>r.json()).then(rows=>{
+    ROWCACHE[tab.id]=rows;
     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)));

← 2c10e98 Growth admin: tab-aware sort control on all data grids (sort  ·  back to Prestige Car Wash  ·  Growth ideas: backfill conservative SWOT + light costs for m cbf5a8b →