← 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> </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 →