← 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
M public/admin/index.html
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 →