[object Object]

← back to Prestige Car Wash

prestige admin: Hootsuite-style Marketing tab (socials composer left, collapsed asset library right) — deployed snapshot

13127d7a1c9463778fc190769e5691ba527acc4b · 2026-07-20 11:20:57 -0700 · Steve Abrams

Files touched

Diff

commit 13127d7a1c9463778fc190769e5691ba527acc4b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 20 11:20:57 2026 -0700

    prestige admin: Hootsuite-style Marketing tab (socials composer left, collapsed asset library right) — deployed snapshot
---
 reports/admin-index.deployed-20260720.html | 532 +++++++++++++++++++++++++++++
 1 file changed, 532 insertions(+)

diff --git a/reports/admin-index.deployed-20260720.html b/reports/admin-index.deployed-20260720.html
new file mode 100644
index 0000000..41bd273
--- /dev/null
+++ b/reports/admin-index.deployed-20260720.html
@@ -0,0 +1,532 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Prestige Car Wash — Growth Command Center</title>
+<link rel="icon" type="image/jpeg" href="/assets/logo-ig.jpg">
+<link rel="apple-touch-icon" href="/assets/logo-ig.jpg">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&display=swap" rel="stylesheet">
+<link rel="stylesheet" href="/assets/pcw.css">
+<style>
+  .tabs{display:flex;gap:6px;overflow-x:auto;padding:10px 0;border-bottom:1px solid var(--line);position:sticky;top:60px;background:rgba(11,15,22,.9);backdrop-filter:blur(8px);z-index:30}
+  .tabs button{white-space:nowrap;background:var(--panel);border:1px solid var(--line);color:var(--mut);border-radius:999px;padding:8px 14px;font-size:13px;font-weight:700;cursor:pointer}
+  .tabs button.on{background:linear-gradient(135deg,var(--brand),var(--brand2));color:#001018;border-color:transparent}
+  .when{margin-top:8px;color:var(--mut);font-size:10.5px;font-variant-numeric:tabular-nums}
+  .stat{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:18px}
+  .stat .s{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 16px;min-width:120px;
+    cursor:pointer;text-align:left;font-family:inherit;color:inherit;transition:border-color .15s,transform .15s,box-shadow .15s}
+  .stat .s:hover,.stat .s:focus-visible{border-color:var(--brand);transform:translateY(-2px);box-shadow:0 6px 18px rgba(40,195,255,.12);outline:none}
+  .stat .s:active{transform:translateY(0)}
+  .stat .s b{font-size:24px;color:var(--brand);display:block}
+  .stat .s span{font-size:12px;color:var(--mut)}
+  .stat .s .goarrow{float:right;color:var(--mut);font-size:12px;opacity:0;transition:opacity .15s}
+  .stat .s:hover .goarrow,.stat .s:focus-visible .goarrow{opacity:1}
+  a.tag{cursor:pointer}
+  a.tag:hover{border-color:var(--brand);color:var(--ink)}
+  .rate{color:var(--gold);font-weight:800}
+  .badge{font-size:10px;border-radius:999px;padding:2px 8px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
+  .b-good{background:rgba(58,210,159,.15);color:var(--good)} .b-warn{background:rgba(255,180,84,.15);color:var(--warn)} .b-bad{background:rgba(255,107,107,.15);color:var(--bad)} .b-mut{background:var(--panel2);color:var(--mut)}
+  .heat{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;max-width:560px}
+  .heat .d{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px;text-align:center;font-size:12px}
+  .heat .d b{display:block;font-size:11px;color:var(--mut)}
+  code{background:var(--panel2);padding:1px 6px;border-radius:6px;font-size:12px}
+  .kv{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:8px 0;font-size:14px}
+</style>
+</head>
+<body>
+<nav class="nav"><div class="wrap row">
+  <div class="brand"><span class="mk"><img src="/assets/logo-ig.jpg" alt="Prestige Car Wash" style="width:100%;height:100%;display:block;object-fit:cover"></span><div>Prestige <small>Admin · Growth Center</small></div></div>
+  <div class="links" style="margin-left:auto"><a href="/" target="_blank">View site ↗</a></div>
+</div></nav>
+
+<div class="wrap">
+  <div class="tabs" id="tabs"></div>
+
+  <div class="controls" id="ctl" style="margin-top:16px">
+    <input type="search" id="q" placeholder="Search…">
+    <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>
+
+  <div id="view" style="padding:8px 0 60px"></div>
+</div>
+
+<script>
+const $=s=>document.querySelector(s);
+function fmtDate(iso){ if(!iso) return '—'; const d=new Date(iso); if(isNaN(d)) return '—';
+  return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
+function when(iso){ return `<div class="when" title="created ${iso||''}">🕓 ${fmtDate(iso)}</div>`; }
+const rating=r=>r?`<span class="rate">★ ${r}</span>`:'';
+const enc=s=>String(s==null?'':s).replace(/</g,'&lt;');
+
+let DENS=localStorage.getItem('pcwadmin.density')||'3';
+$('#density').value=DENS; setDensity(DENS);
+function setDensity(v){v=Number(v);document.documentElement.style.setProperty('--cols',v);
+  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)};
+
+// ---- tab definitions -------------------------------------------------------
+const TABS=[
+  {id:'overview',label:'📊 Overview'},
+  {id:'marketing',label:'🎨 Marketing'},
+  {id:'tips',label:'💵 Tips & Employees'},
+  {id:'specials',label:'📅 Daily Specials'},
+  {id:'competitors',label:'🥊 Competitors',api:'/api/admin/competitors',search:['name','city','notes','format'],
+   card:c=>cardWrap(`<div class="row"><div class="cat">${enc(c.city)} · ${enc(c.format)}</div>${rating(c.rating)}</div>
+     <div class="ttl">${enc(c.name)}</div>
+     <div class="blurb">${enc(c.notes)}</div>
+     <div class="kv"><span>Reviews</span><b>${c.reviews||'—'}</b></div>
+     <div class="kv"><span>Pricing</span><span>${enc(c.price_signal)}</span></div>
+     <div class="tags">${(c.ad_platforms||[]).map(p=>`<span class="tag">📣 ${enc(p)}</span>`).join('')||'<span class="tag b-mut">no ad signals scanned</span>'}</div>
+     ${c.website?`<a class="tag" href="${enc(c.website)}" target="_blank" style="margin-top:6px;display:inline-block">site ↗</a>`:''}
+     ${when(c.created_at)}`)},
+  {id:'best-post',label:'⏰ Best Times to Post'},
+  {id:'best-market',label:'💸 Best Times to Market'},
+  {id:'holidays',label:'🎉 Holiday-Driven',api:'/api/admin/holidays',search:['name','campaign'],
+   card:h=>cardWrap(`<div class="cat">${enc(h.date)} · lead ${h.lead_days}d</div>
+     <div class="ttl">${enc(h.name)}</div><div class="blurb">${enc(h.campaign)}</div>
+     <div class="tags">${(h.channels||[]).map(c=>`<span class="tag">${enc(c)}</span>`).join('')}</div>${when(h.created_at)}`)},
+  {id:'services',label:'🚿 Services',api:'/api/admin/services',search:['name','category','blurb'],
+   card:s=>cardWrap(`<div class="row"><div class="cat">${enc(s.category)} · ${s.duration_min}min</div>${s.featured?'<span class="badge b-good">featured</span>':''}</div>
+     <div class="ttl">${enc(s.name)}</div><div class="blurb">${enc(s.blurb)}</div>
+     <div class="row"><span class="price">${enc(s.price_display||('$'+s.price))}</span>
+       <span class="badge b-mut">${s.media_clip?'media set':'no media'}</span></div>${when(s.created_at)}`)},
+  {id:'partners',label:'🤝 Unlikely Partners',api:'/api/admin/partners',search:['name','type','idea','why'],
+   card:p=>cardWrap(`<div class="row"><div class="cat">${enc(p.type)}</div>
+       <span class="badge ${p.impact>=5?'b-good':p.impact>=4?'b-warn':'b-mut'}">impact ${p.impact} · effort ${p.effort}</span></div>
+     <div class="ttl">${enc(p.name)}</div>
+     <div class="blurb"><b>Play:</b> ${enc(p.idea)}</div>
+     <div class="blurb" style="opacity:.85"><b>Why:</b> ${enc(p.why)}</div>
+     <div class="tags"><span class="tag">${enc(p.address)}</span><span class="tag b-mut">${enc(p.status)}</span></div>${when(p.created_at)}`)},
+  {id:'reviews',label:'⭐ Reviews Intel'},
+  {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)}`)},
+  {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>
+     <div class="kv"><span>Budget/mo</span><b>$${a.budget_month}</b></div>
+     <div class="kv"><span>Targeting</span><span>${enc(a.targeting)}</span></div>
+     <div class="tags">${(a.keywords||[]).map(k=>`<span class="tag">${enc(k)}</span>`).join('')}</div>${when(a.created_at)}`)},
+  {id:'directories',label:'📍 Groups · Nextdoor · Directories',api:'/api/admin/directories',search:['name','notes','type'],
+   card:d=>cardWrap(`<div class="row"><div class="cat">${enc(d.type)} · ${enc(d.priority)}</div>
+       <span class="badge ${d.status==='live'?'b-good':d.status==='submitted'?'b-warn':'b-mut'}">${enc(d.status)}</span></div>
+     <div class="ttl">${enc(d.name)}</div><div class="blurb">${enc(d.notes)}</div>
+     ${d.url?`<a class="tag" href="${enc(d.url)}" target="_blank" style="margin-top:6px;display:inline-block">open ↗</a>`:''}${when(d.created_at)}`)},
+  {id:'google',label:'🟢 Google Business'},
+  {id:'leads',label:'📨 Leads',api:'/api/admin/leads',search:['name','service','message'],
+   card:l=>cardWrap(`<div class="cat">${enc(l.service||'general')}</div>
+     <div class="ttl">${enc(l.name)}</div>
+     <div class="kv"><span>Phone</span><span>${enc(l.phone)}</span></div>
+     <div class="kv"><span>Email</span><span>${enc(l.email)}</span></div>
+     <div class="kv"><span>Vehicle</span><span>${enc(l.vehicle)}</span></div>
+     <div class="blurb">${enc(l.message)}</div>${when(l.created_at)}`)},
+  {id:'credentials',label:'🔑 Credentials'}
+];
+// deep-linkable tabs: #services, #competitors, … — back/forward + shareable URLs
+const hashTab=()=>{const h=location.hash.replace('#','');return TABS.some(t=>t.id===h)?h:'overview';};
+let CUR=hashTab();
+
+$('#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=>{if(!TABS.some(t=>t.id===id))id='overview';
+  CUR=id;document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.id===id));
+  if(location.hash!=='#'+id)history.pushState(null,'','#'+id);
+  $('#q').value='';render();
+  document.querySelector(`#tabs button[data-id="${id}"]`)?.scrollIntoView({inline:'nearest',block:'nearest'});};
+window.addEventListener('popstate',()=>{const id=hashTab();if(id!==CUR){CUR=id;
+  document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.id===id));
+  $('#q').value='';render();}});
+
+function cardWrap(inner){return `<div class="card"><div class="body">${inner}</div></div>`;}
+const gridWrap=cards=>`<div class="grid">${cards}</div>`;
+
+function render(){
+  const tab=TABS.find(t=>t.id===CUR);
+  const showCtl=!!tab.api; $('#ctl').style.display=showCtl?'':'none';
+  if(tab.id==='overview') return renderOverview();
+  if(tab.id==='marketing') return renderMarketing();
+  if(tab.id==='tips') return renderTips();
+  if(tab.id==='specials') return renderSpecials();
+  if(tab.id==='best-post') return renderBestPost();
+  if(tab.id==='best-market') return renderBestMarket();
+  if(tab.id==='google') return renderGoogle();
+  if(tab.id==='credentials') return renderCreds();
+  if(tab.id==='reviews') return renderReviews();
+  // generic data bucket
+  fetch(tab.api).then(r=>r.json()).then(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)));
+    $('#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>';
+  });
+}
+
+function renderOverview(){
+  fetch('/api/health').then(r=>r.json()).then(h=>{
+    const c=h.counts;
+    $('#view').innerHTML=`
+      <div class="stat">
+        ${Object.entries(c).map(([k,v])=>`<button class="s" onclick="go('${k}')" title="Open ${k}" aria-label="Open ${k} (${v})">
+          <span class="goarrow">open →</span><b>${v}</b><span>${k}</span></button>`).join('')}
+      </div>
+      <div class="card"><div class="body">
+        <div class="ttl">Growth command center</div>
+        <div class="blurb">Every bucket below is seeded from live SFV market research. Buckets with a search box are data-driven; the rest (best-times, Google, credentials) are tools. Cards show their created date + time. All write/spend/post actions are gated — see the plan. <b>Tap any count above to jump into that bucket.</b></div>
+        <div class="tags">
+          <span class="tag">Market: San Fernando Valley</span>
+          <a class="tag" href="https://www.instagram.com/prestigecwash/" target="_blank" rel="noopener noreferrer">IG: @prestigecwash ↗</a>
+          <span class="tag">Media: Nano Banana + SeeDance</span>
+          <a class="tag b-mut" href="#credentials" onclick="go('credentials');return false">Google Places: key not set (mock) →</a>
+        </div>
+      </div></div>`;
+    $('#count').textContent='';
+  });
+}
+function renderBestPost(){
+  fetch('/api/admin/best-times').then(r=>r.json()).then(bt=>{
+    const pt=bt.post_times||{};
+    $('#view').innerHTML=`
+      <p class="sub" style="color:var(--mut)">${enc(pt.note||'')}</p>
+      <div class="grid">${Object.entries(pt).filter(([k])=>k!=='note').map(([plat,times])=>cardWrap(
+        `<div class="ttl">${enc(plat)}</div><div class="tags">${(times||[]).map(x=>`<span class="tag">${enc(x)}</span>`).join('')}</div>`
+      )).join('')}</div>`;
+  });
+}
+function renderBestMarket(){
+  fetch('/api/admin/best-times').then(r=>r.json()).then(bt=>{
+    const tr=bt.traffic||{},mk=bt.market_times||{};
+    $('#view').innerHTML=`
+      <div class="card"><div class="body"><div class="ttl">Weekly demand</div>
+        <p class="blurb">${enc(tr.note||'')}</p>
+        <div class="heat">${(tr.by_day||[]).map(d=>`<div class="d" style="border-color:${d.demand>=5?'var(--good)':d.demand>=4?'var(--warn)':'var(--line)'}">
+          <b>${enc(d.day)}</b>${'▮'.repeat(d.demand)}<div style="font-size:10px;color:var(--mut);margin-top:4px">${enc(d.note)}</div></div>`).join('')}</div>
+      </div></div>
+      <div class="card"><div class="body"><div class="ttl">Spend triggers</div>
+        <p class="blurb">${enc(mk.note||'')}</p>
+        ${(mk.triggers||[]).map(x=>`<div class="kv"><span>⚡ ${enc(x)}</span></div>`).join('')}</div></div>`;
+  });
+}
+function renderGoogle(){
+  fetch('/api/admin/places').then(r=>r.json()).then(p=>{
+    const mock=p._mock;
+    $('#view').innerHTML=`
+      ${mock?'<div class="card"><div class="body"><span class="badge b-warn">mock data</span><div class="blurb" style="margin-top:8px">Live data needs <code>GOOGLE_PLACES_API_KEY</code> + <code>PCW_PLACE_ID</code> (Steve-gated setup). Then run <code>npm run read:places</code>.</div></div></div>':''}
+      <div class="card"><div class="body">
+        <div class="ttl">${enc(p.name)}</div>
+        <div class="blurb">${enc(p.address)} ${p.rating?'· '+rating(p.rating)+' ('+(p.reviews_count||0)+')':''}</div>
+        <div class="ttl" style="font-size:14px;margin-top:10px">Hours</div>
+        ${(p.hours||[]).map(h=>`<div class="kv"><span>${enc(h)}</span></div>`).join('')}
+        <div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
+          <button class="btn ghost" onclick="draftUpdate('hours')">✎ Update hours in GBP</button>
+          <button class="btn ghost" onclick="draftUpdate('photos')">✎ Add photos in GBP</button>
+          <button class="btn ghost" onclick="draftUpdate('services')">✎ Update services in GBP</button>
+        </div>
+        <div class="when" id="draftMsg"></div>
+      </div></div>`;
+  });
+}
+window.draftUpdate=field=>{
+  fetch('/api/admin/place/draft-update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({field})})
+    .then(r=>r.json()).then(j=>{$('#draftMsg').textContent=j.message;if(j.url&&j.url.length>28)window.open(j.url,'_blank');});
+};
+function renderReviews(){
+  Promise.all([fetch('/api/admin/places').then(r=>r.json()),fetch('/api/admin/reviews-raw').then(r=>r.json())]).then(([p,raw])=>{
+    $('#view').innerHTML=`
+      <div class="stat">${(raw.platform_stats||[]).map(s=>`<div class="s"><b>${s.rating??'—'}</b><span>${enc(s.source)} (${s.count??'?'} reviews)</span></div>`).join('')}</div>
+      <div class="card"><div class="body">
+        <div class="ttl">⚠ Negatives — ops intel (never shown on the public site)</div>
+        ${(raw.negatives_for_ops||[]).map(r=>`<div style="border-top:1px solid var(--line);padding:10px 0">
+          <div class="row"><b>${enc(r.author||'anon')}</b><span class="badge b-bad">${r.rating??'?'}★ · ${enc(r.date||'')}</span></div>
+          <div class="blurb">“${enc(r.text)}”</div>
+          <div class="tags"><span class="tag b-warn">🔧 ${enc(r.ops_flag)}</span></div></div>`).join('')}
+      </div></div>
+      <div class="card" style="margin-top:14px"><div class="body">
+        <div class="ttl">Published on the site (curated positives)</div>
+        ${(p.reviews||[]).map(r=>`<div class="kv"><span>${enc(r.author)} · ${enc(r.source)} ${enc(r.date||'')}</span><span class="rate">${'★'.repeat(r.rating||5)}</span></div>`).join('')}
+        <div class="tags" style="margin-top:10px">
+          <span class="tag">Staff praised by name: ${(raw.staff_named_in_reviews||[]).map(enc).join(', ')||'—'}</span>
+          <span class="tag">Real price signals: ${(raw.real_price_signals||[]).map(enc).join(' · ')||'—'}</span>
+        </div>
+      </div></div>`;
+    $('#count').textContent='';
+  });
+}
+function renderCreds(){
+  Promise.all([fetch('/api/admin/credentials').then(r=>r.json()),
+               fetch('/api/admin/social').then(r=>r.json()),
+               fetch('/api/admin/employees').then(r=>r.json())]).then(([list,soc,emp])=>{
+    const sbadge=s=>s==='have'?'<span class="badge b-good">have it</span>'
+      :s==='claim'?'<span class="badge b-warn">claim listing</span>':'<span class="badge b-bad">create account</span>';
+    $('#view').innerHTML=`
+      <div class="card"><div class="body">
+        <div class="ttl">📱 Social media accounts</div>
+        <p class="blurb">The account registry — handles, URLs, status. Passwords never live here; they go in the secrets manager and show below as present/last-4 only.</p>
+        ${(soc.social||[]).map(a=>`<div class="kv" style="align-items:start"><span style="max-width:70%"><b>${enc(a.platform)}</b> ${a.handle?`<code>${enc(a.handle)}</code>`:''}
+          <br><span style="color:var(--mut);font-size:12px">${enc(a.notes)}</span>
+          ${a.url?`<br><a href="${enc(a.url)}" target="_blank" style="font-size:12px">open ↗</a>`:''}</span>
+          ${sbadge(a.status)}</div>`).join('')}
+      </div></div>
+      <div class="card" style="margin-top:14px"><div class="body">
+        <div class="ttl">💳 Employee Stripe accounts (tips payout)</div>
+        <p class="blurb">${enc((soc.payments||{}).provider||'Stripe Connect')} — ${enc((soc.payments||{}).platform_account_status||'')}</p>
+        ${(emp.employees||[]).map(e=>`<div class="kv"><span><b>${enc(e.name)}</b> <code>${enc(e.code)}</code></span>
+          ${e.stripe_account_id?`<span class="badge b-good">connected ${enc(e.stripe_account_id.slice(-4))}</span>`:'<span class="badge b-bad">not onboarded</span>'}</div>`).join('')}
+      </div></div>
+      <div class="card" style="margin-top:14px"><div class="body">
+        <div class="ttl">🔑 API keys & secrets</div>
+        <p class="blurb">Presence + last-4 only — full secret values are never shown here (managed via the <code>/secrets</code> skill).</p>
+        ${list.map(k=>`<div class="kv"><span><b>${enc(k.label)}</b><br><span style="color:var(--mut);font-size:12px">${enc(k.purpose)}</span></span>
+          <span>${k.present?`<span class="badge b-good">set ${enc(k.hint)}</span>`:'<span class="badge b-bad">not set</span>'}</span></div>`).join('')}
+      </div></div>`;
+    $('#count').textContent='';
+  });
+}
+
+// ---- Marketing: Hootsuite-style social composer (left) + collapsed asset library (right) ----
+// Caption templates seed the composer from the attached asset.
+const CAPTIONS={
+  Instagram:a=>`✨ ${a&&a.name&&a.name.includes('foam')?'That foam cannon hits different.':'Fresh off the line at Prestige.'} 100% hand wash, Reseda & Saticoy. 📍7601 Reseda Blvd #handcarwash #sanfernandovalley #reseda #detailing`,
+  TikTok:a=>`POV: your car gets the Prestige treatment 🚗💦 #carwash #satisfying #detailing #losangeles #fyp`,
+  Facebook:a=>`The price on the sign is the price you pay — no upsells, every seat touched, guaranteed. Come see us at 7601 Reseda Blvd (Reseda & Saticoy). Open 7 days.`,
+  YouTube:a=>`${a&&a.kind==='video'?'Watch the full hand-wash process':'Prestige Car Wash'} — Reseda's family-owned hand car wash. Flat pricing, full detail, ceramic coating. 7601 Reseda Blvd, Reseda CA.`
+};
+// map any social platform label -> the closest caption template
+function tmplKey(platform){const p=(platform||'').toLowerCase();
+  if(p.includes('insta'))return 'Instagram'; if(p.includes('tiktok'))return 'TikTok';
+  if(p.includes('you'))return 'YouTube'; return 'Facebook';}
+
+const SKEY='pcwadmin.schedule';
+let MKT_ASSET=null, MKT_SOCIALS=[], MKT_SEL=new Set(), MKT_CAP='', MKT_WHEN='';
+const loadQueue=()=>{try{return JSON.parse(localStorage.getItem(SKEY)||'[]')}catch(e){return []}};
+const saveQueue=q=>localStorage.setItem(SKEY,JSON.stringify(q));
+
+function renderMarketing(){
+  Promise.all([fetch('/api/admin/assets').then(r=>r.json()),
+               fetch('/api/admin/social').then(r=>r.json())]).then(([assets,soc])=>{
+    MKT_SOCIALS=soc.social||[];
+    const buckets={};assets.forEach(a=>{(buckets[a.bucket]=buckets[a.bucket]||[]).push(a);});
+    $('#view').innerHTML=`
+    <style>
+      .mkt-layout{display:grid;grid-template-columns:1fr 320px;gap:20px;align-items:start}
+      @media(max-width:820px){.mkt-layout{grid-template-columns:1fr}}
+      .mkt-lib{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px;max-height:82vh;overflow:auto;position:sticky;top:76px}
+      .mkt-lib h4{margin:0 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--mut)}
+      .composer-in{width:100%;background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:10px 12px;font-size:14px;font-family:inherit}
+      .soc-pick{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0 4px}
+      .soc-chip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:999px;padding:6px 12px;cursor:pointer;font-size:13px;font-weight:600;color:var(--mut);user-select:none}
+      .soc-chip.on{background:linear-gradient(135deg,var(--brand),var(--brand2));color:#001018;border-color:transparent}
+      .soc-chip .st{font-size:10px;opacity:.85}
+      .qitem{border-top:1px solid var(--line);padding:12px 0;display:flex;gap:12px;align-items:flex-start}
+      .qthumb{width:54px;height:54px;border-radius:8px;object-fit:cover;background:var(--panel2);flex:none}
+    </style>
+    <div class="mkt-layout">
+      <div id="composer"></div>
+      <div class="mkt-lib">
+        <h4>Media library (${assets.length})</h4>
+        ${Object.entries(buckets).map(([b,list])=>`
+          <details class="facet"><summary>${enc(b)} · ${list.length}</summary>
+          ${list.map(a=>`<div class="opt ${MKT_ASSET&&MKT_ASSET.path===a.path?'on':''}" onclick='pickAsset(${JSON.stringify(a).replace(/'/g,"&#39;")})'>
+            <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${a.kind==='video'?'🎬':'🖼'} ${enc(a.name)}</span>
+            <span class="n">${a.size_kb>1024?(a.size_kb/1024).toFixed(1)+'M':a.size_kb+'K'}</span></div>`).join('')}
+          </details>`).join('')}
+      </div>
+    </div>`;
+    renderComposer();
+    $('#count').textContent='';
+  });
+}
+window.pickAsset=a=>{MKT_ASSET=a;
+  if(!MKT_CAP.trim()){const k=MKT_SEL.size?tmplKey([...MKT_SEL][0]):'Instagram';MKT_CAP=CAPTIONS[k](a);}
+  renderMarketing();};
+window.toggleSocial=p=>{if(MKT_SEL.has(p))MKT_SEL.delete(p);else MKT_SEL.add(p);renderComposer();};
+window.seedCap=k=>{MKT_CAP=CAPTIONS[k](MKT_ASSET||{});const ta=$('#capBox');if(ta)ta.value=MKT_CAP;};
+
+function renderComposer(){
+  const a=MKT_ASSET;
+  if(!MKT_WHEN){const d=new Date(Date.now()+3600000);d.setSeconds(0,0);
+    MKT_WHEN=new Date(d.getTime()-d.getTimezoneOffset()*60000).toISOString().slice(0,16);}
+  $('#composer').innerHTML=`
+    <div class="card"><div class="body">
+      <div class="ttl">🗓 Schedule a post</div>
+      <p class="blurb" style="margin-top:2px">Pick one or more accounts, attach media from the library on the right, write a caption, and queue it for a date & time — Hootsuite-style. Publishing stays manual/gated: at go-time you copy the caption and post to each platform.</p>
+
+      <div class="ttl" style="font-size:13px;margin-top:12px">Publish to</div>
+      <div class="soc-pick">
+        ${MKT_SOCIALS.map(s=>`<span class="soc-chip ${MKT_SEL.has(s.platform)?'on':''}" onclick="toggleSocial('${enc(s.platform).replace(/'/g,"&#39;")}')" title="${enc(s.notes||'')}">
+          ${enc(s.platform)} <span class="st">${s.status==='have'?'✓ live':s.status==='claim'?'claim':'setup'}</span></span>`).join('')}
+      </div>
+
+      <div class="row" style="align-items:flex-start;gap:12px;margin-top:12px">
+        <div style="flex:1;min-width:0">
+          <div class="ttl" style="font-size:13px">Caption</div>
+          <textarea id="capBox" class="composer-in" rows="5" oninput="MKT_CAP=this.value" placeholder="Write your caption… or click an asset to auto-fill from a template.">${enc(MKT_CAP)}</textarea>
+          <div class="tags" style="margin-top:6px">
+            <span class="tag" style="cursor:pointer" onclick="seedCap('Instagram')">+ IG caption</span>
+            <span class="tag" style="cursor:pointer" onclick="seedCap('TikTok')">+ TikTok</span>
+            <span class="tag" style="cursor:pointer" onclick="seedCap('Facebook')">+ Facebook</span>
+            <span class="tag" style="cursor:pointer" onclick="seedCap('YouTube')">+ YouTube</span>
+          </div>
+        </div>
+        <div style="width:150px;flex:none">
+          <div class="ttl" style="font-size:13px">Media</div>
+          ${a?`<div style="border:1px solid var(--line);border-radius:10px;overflow:hidden">
+              ${a.kind==='video'?`<video src="/${a.path}" muted playsinline style="width:100%;display:block"></video>`:`<img src="/${a.path}" style="width:100%;display:block">`}
+            </div><div class="when" style="margin-top:4px" title="${enc(a.name)}">${enc(a.name)}</div>`
+            :`<div style="border:1px dashed var(--line);border-radius:10px;padding:18px 8px;text-align:center;color:var(--mut);font-size:12px">Pick an asset →<br>from the library</div>`}
+        </div>
+      </div>
+
+      <div class="row" style="margin-top:14px;gap:10px;flex-wrap:wrap;justify-content:flex-start">
+        <label style="font-size:13px;color:var(--mut)">When <input id="schWhen" type="datetime-local" class="composer-in" style="width:auto;display:inline-block" value="${enc(MKT_WHEN)}" onchange="MKT_WHEN=this.value"></label>
+        <button class="btn" onclick="schedulePost()">+ Add to queue</button>
+        <button class="btn ghost" onclick="postNow()">Copy & open now</button>
+        <span class="when" id="schMsg"></span>
+      </div>
+    </div></div>
+
+    <div class="card" style="margin-top:14px"><div class="body" id="queueBox"></div></div>`;
+  renderQueue();
+}
+
+function renderQueue(){
+  const q=loadQueue().sort((a,b)=>(a.when||'').localeCompare(b.when||''));
+  const box=$('#queueBox'); if(!box)return;
+  const now=Date.now();
+  box.innerHTML=`<div class="ttl">📋 Scheduled queue (${q.length})</div>
+    ${q.length?q.map(p=>{const due=p.when&&new Date(p.when).getTime()<=now;
+      return `<div class="qitem">
+        ${p.media?`<img class="qthumb" src="/${enc(p.media)}" alt="">`:`<div class="qthumb" style="display:grid;place-items:center;color:var(--mut);font-size:20px">📝</div>`}
+        <div style="flex:1;min-width:0">
+          <div class="row"><div class="tags">${(p.socials||[]).map(s=>`<span class="tag">${enc(s)}</span>`).join('')}</div>
+            <span class="badge ${due?'b-good':'b-warn'}">${due?'ready to post':'scheduled'}</span></div>
+          <div class="blurb" style="margin:6px 0;white-space:pre-wrap">${enc((p.caption||'').slice(0,240))}${(p.caption||'').length>240?'…':''}</div>
+          <div class="row"><span class="when">🕓 ${fmtDate(p.when)}</span>
+            <span style="display:flex;gap:8px">
+              <a class="tag" style="cursor:pointer" onclick="fireQueued('${p.id}')">copy & open</a>
+              <a class="tag b-mut" style="cursor:pointer" onclick="delQueued('${p.id}')">delete</a>
+            </span></div>
+        </div></div>`;}).join('')
+      :'<p class="blurb">Nothing queued yet. Build a post above and hit “Add to queue”.</p>'}`;
+}
+window.schedulePost=()=>{
+  const socials=[...MKT_SEL]; const caption=MKT_CAP.trim(); const when=MKT_WHEN; const msg=$('#schMsg');
+  if(!socials.length){if(msg)msg.textContent='Pick at least one account';return;}
+  if(!caption){if(msg)msg.textContent='Write a caption';return;}
+  if(!when){if(msg)msg.textContent='Pick a date & time';return;}
+  const q=loadQueue();
+  q.push({id:'p'+Date.now().toString(36),socials,caption,when,media:MKT_ASSET?MKT_ASSET.path:'',created_at:new Date().toISOString()});
+  saveQueue(q);
+  MKT_SEL=new Set(); MKT_ASSET=null; MKT_CAP=''; MKT_WHEN='';
+  renderMarketing();
+  setTimeout(()=>{const m=$('#schMsg');if(m)m.textContent='Queued ✓';},60);
+};
+window.delQueued=id=>{saveQueue(loadQueue().filter(p=>p.id!==id));renderQueue();};
+window.fireQueued=id=>{const p=loadQueue().find(x=>x.id===id);if(!p)return;
+  navigator.clipboard.writeText(p.caption||'');
+  (p.socials||[]).forEach(name=>{const s=MKT_SOCIALS.find(x=>x.platform===name);const u=s&&s.url;if(u&&u.length>8)window.open(u,'_blank','noopener');});
+};
+window.postNow=()=>{
+  const socials=[...MKT_SEL]; const caption=MKT_CAP.trim(); const msg=$('#schMsg');
+  if(!socials.length||!caption){if(msg)msg.textContent='Pick account(s) + write a caption first';return;}
+  navigator.clipboard.writeText(caption);
+  socials.forEach(name=>{const s=MKT_SOCIALS.find(x=>x.platform===name);const u=s&&s.url;if(u&&u.length>8)window.open(u,'_blank','noopener');});
+  if(msg)msg.textContent='Caption copied ✓ — opened the accounts that have a live URL';
+};
+
+// ---- Tips & Employees: roster, QR cards, ledger ----------------------------
+function renderTips(){
+  Promise.all([fetch('/api/admin/employees').then(r=>r.json()),fetch('/api/admin/tips').then(r=>r.json())]).then(([doc,ledger])=>{
+    const emps=doc.employees||[];
+    const totals={};ledger.filter(t=>t.status!=='error').forEach(t=>t.codes.forEach(c=>{
+      totals[c]=(totals[c]||0)+Math.round(t.amount_cents/t.codes.length);}));
+    $('#view').innerHTML=`
+      <div class="card"><div class="body">
+        <div class="ttl">Team roster — 3-digit tip codes</div>
+        <p class="blurb">Each person's code goes on their badge + printable QR card. Customers hit <code>/tip</code>, punch the code (or scan), and tip by card. Two-person jobs split 50/50.</p>
+        <div class="grid" style="--cols:3">
+        ${emps.map(e=>`<div class="card"><div class="body" style="text-align:center">
+          <div style="font-size:30px;font-weight:900;letter-spacing:6px;color:var(--brand)">${enc(e.code)}</div>
+          <div class="ttl">${enc(e.name)} ${e.active?'':'<span class="badge b-bad">inactive</span>'}</div>
+          <div class="blurb">${enc(e.role||'')}</div>
+          <img src="/api/qr/${enc(e.code)}" alt="QR for ${enc(e.name)}" style="width:140px;margin:8px auto;border-radius:8px;background:#fff;padding:6px">
+          <div class="kv"><span>Tips recorded</span><b>$${((totals[e.code]||0)/100).toFixed(2)}</b></div>
+          <div class="kv"><span>Stripe</span>${e.stripe_account_id?'<span class="badge b-good">connected</span>':'<span class="badge b-bad">not onboarded</span>'}</div>
+          <button class="btn ghost" style="margin-top:8px" onclick="printQR('${enc(e.code)}','${enc(e.name)}')">🖨 Print QR card</button>
+        </div></div>`).join('')}
+        </div>
+      </div></div>
+      <div class="card" style="margin-top:14px"><div class="body">
+        <div class="ttl">➕ Add / update team member</div>
+        <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:10px">
+          <input id="eCode" placeholder="Code (e.g. 103)" maxlength="3" style="width:110px" class="admin-in">
+          <input id="eName" placeholder="Name" class="admin-in">
+          <input id="eRole" placeholder="Role (optional)" class="admin-in">
+          <button class="btn" onclick="saveEmp()">Save</button>
+        </div><div class="when" id="empMsg"></div>
+      </div></div>
+      <div class="card" style="margin-top:14px"><div class="body">
+        <div class="ttl">📒 Tip ledger (${ledger.length})</div>
+        ${ledger.length?ledger.slice(0,50).map(t=>`<div class="kv"><span>${t.names.map(enc).join(' + ')} ${t.split?'<span class="badge b-mut">split</span>':''}
+          <span class="badge ${t.status==='checkout-created'?'b-good':t.status==='offline-intent'?'b-warn':'b-bad'}">${enc(t.status)}</span></span>
+          <span><b>$${(t.amount_cents/100).toFixed(2)}</b> <span style="color:var(--mut);font-size:11px">${fmtDate(t.created_at)}</span></span></div>`).join('')
+        :'<p class="blurb">No tips yet — Stripe key not set, so the page runs in "cash for now" mode. Set <code>STRIPE_SECRET_KEY</code> to go live (gated).</p>'}
+      </div></div>
+      <style>.admin-in{background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:9px 12px;font-size:14px}</style>`;
+    $('#count').textContent='';
+  });
+}
+window.saveEmp=()=>{
+  fetch('/api/admin/employees',{method:'POST',headers:{'Content-Type':'application/json'},
+    body:JSON.stringify({code:$('#eCode').value,name:$('#eName').value,role:$('#eRole').value})})
+    .then(r=>r.json()).then(j=>{ if(j.ok)renderTips(); else $('#empMsg').textContent=j.error; });
+};
+window.printQR=(code,name)=>{
+  const w=window.open('','_blank','width=420,height=600');
+  w.document.write(`<html><head><title>Tip ${name}</title></head>
+    <body style="font-family:Inter,system-ui;text-align:center;padding:30px">
+    <h2 style="margin:0">Enjoyed the wash?</h2>
+    <p style="font-size:18px;margin:6px 0 16px">Tip <b>${name}</b> — no cash needed</p>
+    <img src="/api/qr/${code}" style="width:280px"><div style="font-size:34px;font-weight:900;letter-spacing:8px;margin-top:10px">${code}</div>
+    <p style="color:#555">Scan, or go to prestigecarwash.agentabrams.com/tip and enter the code</p>
+    <script>setTimeout(()=>print(),400)<\/script></body></html>`);
+};
+
+// ---- Daily specials editor --------------------------------------------------
+const DAYS=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
+function renderSpecials(){
+  fetch('/api/admin/specials').then(r=>r.json()).then(sp=>{
+    $('#view').innerHTML=`
+      <div class="card"><div class="body">
+        <div class="ttl">Banner override (beats the weekday rotation)</div>
+        <p class="blurb">Whatever you type here shows in the gold banner on every public page until you clear it. Leave empty to use the weekday rotation below.</p>
+        <div style="display:flex;gap:10px;flex-wrap:wrap">
+          <input id="ovr" value="${enc(sp.override||'')}" placeholder="e.g. 🎆 4th of July weekend: $5 off every full service!"
+            style="flex:1;min-width:260px;background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:10px 12px;font-size:14px">
+          <button class="btn" onclick="saveSpecials()">Save</button>
+        </div>
+      </div></div>
+      <div class="card" style="margin-top:14px"><div class="body">
+        <div class="ttl">Weekday rotation</div>
+        ${DAYS.map((d,i)=>`<div class="kv" style="gap:14px"><span style="min-width:90px"><b>${d}</b></span>
+          <input data-day="${i}" value="${enc((sp.by_weekday||{})[String(i)]||'')}"
+            style="flex:1;background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:8px 12px;font-size:13px"></div>`).join('')}
+        <div style="margin-top:12px;display:flex;gap:10px;align-items:center"><button class="btn" onclick="saveSpecials()">Save all</button><span class="when" id="spMsg"></span></div>
+      </div></div>`;
+    $('#count').textContent='';
+  });
+}
+window.saveSpecials=()=>{
+  const by={};document.querySelectorAll('[data-day]').forEach(i=>by[i.dataset.day]=i.value);
+  fetch('/api/admin/specials',{method:'POST',headers:{'Content-Type':'application/json'},
+    body:JSON.stringify({override:$('#ovr').value,by_weekday:by})})
+    .then(r=>r.json()).then(()=>{const m=$('#spMsg');if(m)m.textContent='Saved ✓ — live on the site now';});
+};
+render();
+</script>
+</body>
+</html>

← 62c7522 Update Prestige business phone to (747) 265-6293 in places.j  ·  back to Prestige Car Wash  ·  scope: real social auto-posting scheduler for prestige marke 5683565 →