[object Object]

← back to Dw Activation Calendar

Activation calendar: vendor filter + see-all-staged list + cross-month reschedule

5e3b839ff4d78823e78544c11940491a1cb2b4c5 · 2026-07-15 13:59:52 -0700 · Steve

- Vendor dropdown (sorted by staged count) filters the calendar to one vendor;
  summary switches to that vendor's staged total + first/last go-live + days used
- 'See all N staged' opens a flat list of every staged item for that vendor
  (across all dates), each row with a date input to reschedule that SKU
- chip modal now has a 'Reschedule go-live' date input → moves to ANY date
  (cross-month, which drag alone can't do)
- verified headless: vendor filter isolates chips, list shows 4405 rows, modal
  reschedule present, 0 console errors

Files touched

Diff

commit 5e3b839ff4d78823e78544c11940491a1cb2b4c5
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 15 13:59:52 2026 -0700

    Activation calendar: vendor filter + see-all-staged list + cross-month reschedule
    
    - Vendor dropdown (sorted by staged count) filters the calendar to one vendor;
      summary switches to that vendor's staged total + first/last go-live + days used
    - 'See all N staged' opens a flat list of every staged item for that vendor
      (across all dates), each row with a date input to reschedule that SKU
    - chip modal now has a 'Reschedule go-live' date input → moves to ANY date
      (cross-month, which drag alone can't do)
    - verified headless: vendor filter isolates chips, list shows 4405 rows, modal
      reschedule present, 0 console errors
---
 public/index.html | 103 +++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 95 insertions(+), 8 deletions(-)

diff --git a/public/index.html b/public/index.html
index 94dea0a..18bccd6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -213,6 +213,23 @@
   .skud .links a{flex:1;min-width:150px;text-align:center;text-decoration:none;font-weight:600;font-size:13.5px;padding:10px 16px;border-radius:10px}
   .skud .links a.adm{background:var(--plum);color:#fff}.skud .links a.adm:hover{background:#4d2230}
   .skud .links a.sf{background:var(--surface);border:1px solid var(--hairline);color:var(--ink)}.skud .links a.sf:hover{border-color:var(--faint)}
+  /* reschedule row in the SKU modal */
+  .skud .resched{display:flex;align-items:center;gap:8px;margin:6px 0 2px;padding:10px 0 0;border-top:1px solid var(--hairline)}
+  .skud .resched label{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);flex:1}
+  .skud .resched input[type=date]{font:inherit;font-size:13px;border:1px solid var(--hairline);border-radius:8px;padding:7px 9px;color:var(--ink);background:var(--surface)}
+  .skud .resched .cta-btn{padding:9px 16px;font-size:11px}
+  /* vendor "see all staged" list */
+  #skuCtrls select#vendorFilter{font:inherit;font-size:13px;color:var(--ink);background:var(--surface);border:1px solid var(--hairline);border-radius:8px;padding:7px 9px;max-width:200px}
+  .linklike{color:var(--plum);text-decoration:none;font-size:12px;font-weight:600}
+  .linklike:hover{text-decoration:underline}
+  .vlist .vrows{max-height:560px;overflow:auto;display:flex;flex-direction:column;gap:6px}
+  .vrow{display:flex;align-items:center;gap:11px;padding:6px 8px;border:1px solid var(--hairline);border-radius:10px;background:var(--ground)}
+  .vthumb{width:44px;height:44px;flex:0 0 auto;border-radius:7px;overflow:hidden;background:var(--surface);border:1px solid var(--hairline)}
+  .vthumb img,.vthumb .ph{width:100%;height:100%;object-fit:cover;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700}
+  .vmeta{flex:1;min-width:0}
+  .vlabel{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+  .vsub{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+  .vdate{font:inherit;font-size:12px;border:1px solid var(--hairline);border-radius:7px;padding:6px 8px;color:var(--ink);background:var(--surface);flex:0 0 auto}
 </style>
 </head>
 <body>
@@ -229,6 +246,8 @@
     <div class="ctrl"><label>Per vendor/day</label><input id="perVendorDay" type="number" value="500" min="1"></div>
     <div class="ctrl"><label>Start</label><input id="start" type="date"></div>
     <button id="apply">Apply</button>
+    <div class="ctrl"><label>Vendor</label><select id="vendorFilter"><option value="">All vendors</option></select></div>
+    <button id="vendorAll" class="btn-pill" style="display:none">See all staged</button>
   </div>
   <button class="btn-primary" id="gnew">+ New Event</button>
 </header>
@@ -498,7 +517,7 @@ document.getElementById('gtoday').onclick=()=>{const n=new Date();gMonth=new Dat
 document.getElementById('gnew').onclick=()=>{const n=new Date();openEventModal(null,n.toISOString().slice(0,10));};
 
 /* ======================= SKU ACTIVATION (existing logic) ======================= */
-let DATA=null, viewMonth=null;
+let DATA=null, viewMonth=null, vendorFilter='';
 const VCOLORS={};
 function vcolor(v){
   if(VCOLORS[v]) return VCOLORS[v];
@@ -512,13 +531,25 @@ async function load(){
   const qs=new URLSearchParams({perDay,perVendorDay}); if(start) qs.set('start',start);
   document.getElementById('cal').innerHTML='<div class="loading">Scheduling …</div>';
   const r=await fetch('/api/schedule?'+qs); DATA=await r.json();
-  renderSummary(); renderLegend();
+  renderSummary(); renderLegend(); populateVendorFilter();
   const first=DATA.days[0]?new Date(DATA.days[0].date+'T00:00:00'):new Date();
   viewMonth=new Date(first.getFullYear(),first.getMonth(),1);
   renderMonth();
 }
 function renderSummary(){
   const s=DATA;
+  if(vendorFilter){
+    let count=0; const dates=[];
+    s.days.forEach(d=>{ const n=d.skus.filter(x=>x.vendor===vendorFilter).length; if(n){ count+=n; dates.push(d.date); } });
+    dates.sort();
+    document.getElementById('summary').innerHTML=
+      `<div class="k"><span>${esc(vendorFilter)} — staged</span><b>${count.toLocaleString()}</b></div>`+
+      `<div class="k"><span>Days used</span><b>${dates.length}</b></div>`+
+      `<div class="k"><span>First go-live</span><b>${dates[0]||'—'}</b></div>`+
+      `<div class="k"><span>Last go-live</span><b>${dates[dates.length-1]||'—'}</b></div>`+
+      `<div class="k"><span>&nbsp;</span><b><a href="#" class="linklike" onclick="clearVendorFilter();return false">← all vendors</a></b></div>`;
+    return;
+  }
   document.getElementById('summary').innerHTML=
     `<div class="k"><span>Staged for active</span><b>${s.totalSkus.toLocaleString()}</b></div>`+
     `<div class="k"><span>Vendors</span><b>${Object.keys(s.vendorTotals).length}</b></div>`+
@@ -527,6 +558,48 @@ function renderSummary(){
     `<div class="k"><span>Cadence</span><b>${s.perDay}/day</b></div>`+
     (s.coverage?`<div class="k"><span>Date+time assigned</span><b title="every staged item has a durable go-live date and time">${s.coverage} ✓</b></div>`:'');
 }
+// ---- Vendor sort/filter + "see all staged for this vendor" ----
+function populateVendorFilter(){
+  const sel=document.getElementById('vendorFilter'); if(!sel||!DATA) return;
+  const cur=sel.value;
+  const vts=Object.entries(DATA.vendorTotals||{}).sort((a,b)=>b[1]-a[1]);
+  sel.innerHTML='<option value="">All vendors ('+(DATA.totalSkus||0).toLocaleString()+')</option>'+
+    vts.map(([v,n])=>`<option value="${esc(v)}">${esc(v)} (${n.toLocaleString()})</option>`).join('');
+  sel.value=cur;
+}
+function applyVendorFilter(v){
+  vendorFilter=v||'';
+  const sel=document.getElementById('vendorFilter'); if(sel) sel.value=vendorFilter;
+  const btn=document.getElementById('vendorAll');
+  if(btn){ btn.style.display=vendorFilter?'':'none';
+    btn.textContent=vendorFilter?('See all '+((DATA.vendorTotals||{})[vendorFilter]||0).toLocaleString()+' staged'):'See all staged'; }
+  renderSummary(); renderMonth();
+}
+function clearVendorFilter(){ applyVendorFilter(''); }
+// Flat list of EVERY staged item for a vendor (across all dates) with per-row reschedule.
+function openVendorList(vendor){
+  const items=DATA.days.flatMap(d=>d.skus.filter(x=>x.vendor===vendor))
+    .sort((a,b)=> (a.go_live_at<b.go_live_at?-1:a.go_live_at>b.go_live_at?1:0));
+  const rows=items.map(s=>{
+    const key=s.shopify_id||s.dw_sku;
+    const img=(s.image_url&&/^https?:/.test(s.image_url))
+      ? `<img src="${esc(s.image_url)}" loading="lazy">`
+      : `<div class="ph" style="background:${vcolor(vendor)}">${esc(vendor.slice(0,3).toUpperCase())}</div>`;
+    return `<div class="vrow"><div class="vthumb">${img}</div>
+      <div class="vmeta"><div class="vlabel">${esc(s.title||skuLabel(s))}</div>
+        <div class="vsub">${esc(skuLabel(s))} · ${esc(s.fix_type)}</div></div>
+      <input type="date" class="vdate" data-key="${esc(key)}" value="${esc((s.go_live_at||'').slice(0,10))}" title="reschedule go-live"></div>`;
+  }).join('');
+  const body=`<div class="mform vlist"><div class="mhead"><div class="mtitle">${esc(vendor)} — ${items.length.toLocaleString()} staged</div><button type="button" class="mclose" id="vl_x" aria-label="Close">✕</button></div>
+    <div class="note" style="margin:6px 0 8px">All staged items for this vendor, in go-live order. Change any date to reschedule that SKU.</div>
+    <div class="vrows">${rows||'<div class="loading">None staged.</div>'}</div></div>`;
+  const M=ModalRig.open({key:'dwcal-vendorlist',title:vendor,bodyHtml:body,width:560,height:720,bg:'var(--surface)'});
+  M.body.querySelector('#vl_x').onclick=M.close;
+  M.body.querySelectorAll('.vdate').forEach(inp=>{
+    inp.onchange=async()=>{ const to=inp.value; if(!to) return; inp.disabled=true;
+      await moveSku(inp.dataset.key, to, inp.dataset.key); M.close(); openVendorList(vendor); };
+  });
+}
 function renderLegend(){
   const vt=DATA.vendorTotals;
   const top=Object.entries(vt).sort((a,b)=>b[1]-a[1]);
@@ -563,13 +636,21 @@ function openSkuModal(s){
       <dt>Goes active</dt><dd title="${esc(s.go_live_at||'')}">⏱ ${fmtGoLive(s.go_live_at)}${s.slot_hour!=null?' · '+slotLabel(s.slot_hour)+' slot':''}</dd>
       ${idNum?`<dt>Shopify ID</dt><dd><code>${esc(idNum)}</code></dd>`:''}
     </dl>
+    <div class="resched">
+      <label for="sk_date">Reschedule go-live</label>
+      <input type="date" id="sk_date" value="${esc((s.go_live_at||'').slice(0,10))}">
+      <button type="button" id="sk_move" class="cta-btn">Move</button>
+    </div>
     <div class="links">
       ${adminUrl?`<a class="adm" href="${adminUrl}" target="_blank" rel="noopener noreferrer">Open in Shopify Admin</a>`:''}
       ${siteUrl?`<a class="sf" href="${siteUrl}" target="_blank" rel="noopener noreferrer" title="staged items 404 on the storefront until they go active">View on site</a>`:''}
     </div>
   </div>`;
-  const M=ModalRig.open({key:'dwcal-sku',title:'SKU',bodyHtml,width:440,height:620,bg:'var(--surface)'});
+  const M=ModalRig.open({key:'dwcal-sku',title:'SKU',bodyHtml,width:440,height:660,bg:'var(--surface)'});
   M.body.querySelector('#sk_x').onclick=M.close;
+  const mv=M.body.querySelector('#sk_move'), dinp=M.body.querySelector('#sk_date');
+  if(mv) mv.onclick=async()=>{ const to=dinp.value; if(!to){ return; } mv.disabled=true; mv.textContent='Moving…';
+    await moveSku(s.shopify_id||s.dw_sku, to, label); M.close(); };
 }
 
 /* Generic SKU-schedule month grid — shared by the SKU Activation tab AND the
@@ -592,8 +673,12 @@ function renderSchedMonth(containerId, monthDate, data){
     const iso=`${y}-${String(m+1).padStart(2,'0')}-${String(dn).padStart(2,'0')}`;
     const day=byDate[iso];
     const cell=document.createElement('div'); cell.className='cell scell'+(iso===todayISO?' today':'');
-    const spanTxt=(day&&day.firstSlot!=null)?`<span class="span" title="drain slots that fire this day">${slotLabel(day.firstSlot)}–${slotLabel(day.lastSlot)}</span>`:'';
-    cell.innerHTML=`<div class="dh"><span class="dn">${dn}</span>${day?`<span class="dhr"><span class="cnt">${day.count}</span>${spanTxt}</span>`:''}</div>`;
+    // vendor filter: show only the selected vendor's chips (and count) on each day
+    const dayskus = day ? (vendorFilter ? day.skus.filter(x=>x.vendor===vendorFilter) : day.skus) : null;
+    const shownCount = dayskus ? dayskus.length : 0;
+    let spanTxt='';
+    if(shownCount){ const hrs=dayskus.map(x=>x.slot_hour); spanTxt=`<span class="span" title="drain slots that fire this day">${slotLabel(Math.min(...hrs))}–${slotLabel(Math.max(...hrs))}</span>`; }
+    cell.innerHTML=`<div class="dh"><span class="dn">${dn}</span>${shownCount?`<span class="dhr"><span class="cnt">${shownCount}</span>${spanTxt}</span>`:''}</div>`;
     // --- drop target: dropping a chip here reschedules that SKU to this day ---
     cell.addEventListener('dragover',ev=>{ ev.preventDefault(); ev.dataTransfer.dropEffect='move'; cell.classList.add('drop-target'); });
     cell.addEventListener('dragleave',ev=>{ if(!cell.contains(ev.relatedTarget)) cell.classList.remove('drop-target'); });
@@ -602,9 +687,9 @@ function renderSchedMonth(containerId, monthDate, data){
       let d; try{ d=JSON.parse(ev.dataTransfer.getData('text/plain')||'{}'); }catch(e){ return; }
       if(d.key && d.from!==iso) moveSku(d.key, iso, d.label);
     });
-    if(day){
+    if(shownCount){
       const chips=document.createElement('div'); chips.className='chips';
-      day.skus.forEach(s=>{
+      dayskus.forEach(s=>{
         // Every data point is a REAL anchor to its actual staged product (Shopify
         // admin — staged items 404 on the storefront until active). So a chip is
         // openable every way: hover shows the URL, cmd/ctrl/middle/right-click opens
@@ -646,7 +731,7 @@ function renderSchedMonth(containerId, monthDate, data){
       cell.appendChild(chips);
       // expand / collapse the whole day column (hidden later if everything fits)
       const more=document.createElement('button'); more.type='button'; more.className='more';
-      const closedLabel='▾ show all '+day.count;
+      const closedLabel='▾ show all '+shownCount;
       more.textContent=closedLabel;
       more.onclick=()=>{
         const open=cell.classList.toggle('expanded');
@@ -687,6 +772,8 @@ function renderMonth(){
 document.getElementById('prev').onclick=()=>{viewMonth=new Date(viewMonth.getFullYear(),viewMonth.getMonth()-1,1);renderMonth();};
 document.getElementById('next').onclick=()=>{viewMonth=new Date(viewMonth.getFullYear(),viewMonth.getMonth()+1,1);renderMonth();};
 document.getElementById('apply').onclick=load;
+document.getElementById('vendorFilter').onchange=(e)=>applyVendorFilter(e.target.value);
+document.getElementById('vendorAll').onclick=()=>{ if(vendorFilter) openVendorList(vendorFilter); };
 
 /* ======================= boot ======================= */
 // Default to the populated SKU Activation tab so the page shows content on first

← ca49220 Activation calendar: drag SKU chips between days to reschedu  ·  back to Dw Activation Calendar  ·  auto-save: 2026-07-15T14:07:06 (3 files) — data/activation-s 4074d9f →