[object Object]

← back to Prestige Car Wash

Growth admin: expandable SWOT · Costs · Payback panel on idea cards (dtd:A, contrarian-gated)

ed6822215e8fc2b0424a4a3260524313108e2905 · 2026-07-26 07:34:57 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit ed6822215e8fc2b0424a4a3260524313108e2905
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sun Jul 26 07:34:57 2026 -0700

    Growth admin: expandable SWOT · Costs · Payback panel on idea cards (dtd:A, contrarian-gated)
---
 public/admin/index.html | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 59 insertions(+), 1 deletion(-)

diff --git a/public/admin/index.html b/public/admin/index.html
index 33e5fc2..a1ecddd 100644
--- a/public/admin/index.html
+++ b/public/admin/index.html
@@ -53,6 +53,26 @@
   .k-brand-promo{background:rgba(10,132,255,.16);color:var(--brand)}
   .k-hyperframes-motion{background:rgba(255,180,84,.16);color:var(--warn)}
   .k-internal-pitch{background:var(--panel2);color:var(--mut)}
+  /* --- idea analysis panel (SWOT · costs · payback) --- */
+  details.analysis{margin-top:10px;border:1px solid var(--line);border-radius:10px;background:var(--panel2);overflow:hidden}
+  details.analysis>summary{cursor:pointer;list-style:none;padding:9px 12px;font-size:12.5px;font-weight:700;color:var(--brand);user-select:none}
+  details.analysis>summary::-webkit-details-marker{display:none}
+  details.analysis>summary::before{content:'▸ ';font-weight:800}
+  details.analysis[open]>summary::before{content:'▾ '}
+  .analysis-body{padding:4px 12px 12px}
+  .swot-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:6px 0 10px}
+  .swot-col{border-radius:8px;padding:8px 10px;font-size:12.5px}
+  .swot-col h5{margin:0 0 5px;font-size:11px;text-transform:uppercase;letter-spacing:.4px}
+  .swot-col ul{margin:0;padding-left:15px} .swot-col li{margin:3px 0;line-height:1.35}
+  .s-str{background:rgba(48,209,88,.10)} .s-str h5{color:var(--good,#30d158)}
+  .s-wk{background:rgba(255,180,84,.10)} .s-wk h5{color:var(--warn,#ffb454)}
+  .s-op{background:rgba(10,132,255,.10)} .s-op h5{color:var(--brand)}
+  .s-th{background:rgba(255,69,58,.10)} .s-th h5{color:#ff453a}
+  .cost-card{border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin:8px 0}
+  .cost-card h5{margin:0 0 4px;font-size:12px;text-transform:capitalize}
+  .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}}
 </style>
 </head>
 <body>
@@ -171,7 +191,7 @@ 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>${when(s.created_at)}`)},
+     <div class="ttl">${enc(s.title)}</div><div class="blurb">${enc(s.rationale)}</div>${analysisPanel(s)}${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>
@@ -201,6 +221,44 @@ window.go=id=>{CUR=id;document.querySelectorAll('#tabs button').forEach(b=>b.cla
   $('#q').value='';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
+// sub-objects that exist, so ideas without analysis are unaffected (additive).
+function swotCol(title,arr,cls){
+  if(!Array.isArray(arr)||!arr.length) return '';
+  return `<div class="swot-col ${cls}"><h5>${title}</h5><ul>${arr.map(x=>`<li>${enc(x)}</li>`).join('')}</ul></div>`;
+}
+function costRows(obj){
+  return Object.entries(obj).filter(([k,v])=>k!=='recommended'&&k!=='note'&&typeof v!=='object')
+    .map(([k,v])=>`<div class="kv"><span>${enc(k.replace(/_/g,' '))}</span><span>${enc(v)}</span></div>`).join('');
+}
+function analysisPanel(s){
+  const w=s.swot, c=s.costs; if(!w && !c) return '';
+  const hasPricing = c && Object.entries(c).some(([k,v])=>k!=='payback_model'&&v&&typeof v==='object'&&'recommended' in v);
+  const hasPayback = c && c.payback_model && typeof c.payback_model==='object';
+  let html='';
+  if(w){
+    html+=`<div class="swot-grid">${swotCol('💪 Strengths',w.strengths,'s-str')}${swotCol('⚠️ Weaknesses',w.weaknesses,'s-wk')}${swotCol('🚀 Opportunities',w.opportunities,'s-op')}${swotCol('🛑 Threats',w.threats,'s-th')}</div>`;
+  }
+  if(c){
+    if(c.note) html+=`<div class="blurb" style="font-style:italic;margin:4px 0">${enc(c.note)}</div>`;
+    for(const [key,val] of Object.entries(c)){
+      if(key==='note'||!val||typeof val!=='object') continue;
+      // payback_model is a MODELLED OUTCOME, not a purchasing option — label it so
+      // a client never misreads it as a third price. (contrarian gate, 2026-07-26)
+      if(key==='payback_model'){
+        html+=`<div class="cost-card model"><h5>📈 Payback model <span class="badge b-mut">modelled · not a price</span></h5>${costRows(val)}</div>`;
+        continue;
+      }
+      const rec = val.recommended===true?' <span class="badge b-good">recommended</span>'
+                : val.recommended===false?' <span class="badge b-mut">fallback</span>':'';
+      html+=`<div class="cost-card"><h5>${enc(key.replace(/_/g,' '))}${rec}</h5>${costRows(val)}</div>`;
+    }
+  }
+  // Honest summary — only name sub-sections that are actually populated.
+  const parts=[]; if(w) parts.push('SWOT'); if(hasPricing) parts.push('Costs'); if(hasPayback) parts.push('Payback');
+  const summary = parts.join(' · ') || 'Analysis';
+  return `<details class="analysis"><summary>${summary}</summary><div class="analysis-body">${html}</div></details>`;
+}
 const gridWrap=cards=>`<div class="grid">${cards}</div>`;
 
 function render(){

← 78ccc70 lhf: contrarian fixes — panel note replaces per-card filler,  ·  back to Prestige Car Wash  ·  auto-save: 2026-07-26T07:41:30 (2 files) — public/admin/inde 6edbd5d →