🌾 LiveStalksMarketMaker

'); } // ---- Phase 4: pricing suggestions ---- function pricingSuggest(){ const {m,max}=heatStats(); const avg=booths.length?booths.reduce((s,b)=>s+bFee(b),0)/booths.length:0; const sugg=[]; booths.forEach(b=>{ const heat=m[b.id]/max,corner=isCornerB(b); if((heat>0.7||corner)&&bFee(b)<=avg*1.1){ const add=Math.round(bFee(b)*((corner&&heat>0.7)?0.3:0.2)/5)*5; if(add>0) sugg.push({b,cur:bFee(b),add,why:[corner?'corner':null,heat>0.7?'high traffic':null].filter(Boolean).join(' + ')}); } }); sugg.sort((a,b)=>b.add-a.add); const uplift=sugg.reduce((s,o)=>s+o.add,0); const rows=sugg.slice(0,12).map(o=>'#'+esc(o.b.label)+''+esc(o.why)+'$'+o.cur+'$'+(o.cur+o.add)+'').join('')||'No clear premium opportunities yet — place entrances (+ Entry) so traffic can be modeled.'; const mc=document.getElementById('mcard'); mc.innerHTML='

💡 Pricing suggestions

Corner and high-traffic booths priced at or below average are premium candidates. Potential uplift: $'+uplift+'/market day.
'+rows+'
BoothWhyNowSuggested
'; document.getElementById('modal').classList.add('show'); document.getElementById('pClose').onclick=()=>document.getElementById('modal').classList.remove('show'); const pa=document.getElementById('pApply'); if(pa)pa.onclick=()=>{sugg.forEach(o=>{o.b.fee=(o.b.fee||0)+o.add;});document.getElementById('modal').classList.remove('show');commit();}; } // ---- Phase 3: share / embed ---- function sharePanel(){ const mc=document.getElementById('mcard'); const base=location.href.split('#')[0].split('?')[0]; const shopUrl=base+'?view=shopper'; const vendUrl=base+'?view=vendor'+(vendorViewId?('&v='+vendorViewId):''); mc.innerHTML='

🔗 Share

These open the same file in a read-only kiosk mode (great for a market-entrance screen or a vendor link). In production they become hosted URLs you can post or embed; live links & multi-editor sync need the backend.
'; document.getElementById('modal').classList.add('show'); const cp=t=>{try{navigator.clipboard.writeText(t);}catch(e){}}; document.getElementById('cs').onclick=()=>{cp(shopUrl);document.getElementById('cs').textContent='Copied!';}; document.getElementById('cvv').onclick=()=>{cp(vendUrl);document.getElementById('cvv').textContent='Copied!';}; document.getElementById('mc2').onclick=()=>document.getElementById('modal').classList.remove('show'); } // ---- view helpers ---- function fit(){ const all=[...booths,...zones]; if(!all.length){scale=8;offX=80;offY=120;draw();return;} let minx=Infinity,miny=Infinity,maxx=-Infinity,maxy=-Infinity; all.forEach(o=>{minx=Math.min(minx,o.x);miny=Math.min(miny,o.y);maxx=Math.max(maxx,o.x+o.w);maxy=Math.max(maxy,o.y+o.h);}); const pad=26,W=cv.clientWidth-pad*2,H=cv.clientHeight-pad*2; scale=Math.min(40,Math.max(2,Math.min(W/(maxx-minx||1),H/(maxy-miny||1)))); offX=pad-minx*scale+(W-(maxx-minx)*scale)/2; offY=pad-miny*scale+(H-(maxy-miny)*scale)/2; draw(); } function zoomBtn(f){const cx=cv.clientWidth/2,cy=cv.clientHeight/2,wx=s2wx(cx),wy=s2wy(cy);scale=Math.min(40,Math.max(2,scale*f));offX=cx-wx*scale;offY=cy-wy*scale;draw();} // ---- print / export ---- function buildExport(){ const all=[...booths,...zones]; if(!all.length)return null; let minx=Infinity,miny=Infinity,maxx=-Infinity,maxy=-Infinity; all.forEach(o=>{minx=Math.min(minx,o.x);miny=Math.min(miny,o.y);maxx=Math.max(maxx,o.x+o.w);maxy=Math.max(maxy,o.y+o.h);}); const pad=44,targetW=1200,wW=(maxx-minx)||1,wH=(maxy-miny)||1,sc=Math.min(40,(targetW-2*pad)/wW); const cW=Math.round(wW*sc+2*pad),cH=Math.round(wH*sc+2*pad); const cn=document.createElement('canvas');cn.width=cW;cn.height=cH;const c=cn.getContext('2d'); if(!fillSurface(c,cW,cH,surface)){c.fillStyle='#ffffff';c.fillRect(0,0,cW,cH);} const X=x=>(x-minx)*sc+pad,Y=y=>(y-miny)*sc+pad; zones.forEach(z=>paintZone(c,z,X,Y,sc)); const ov=overlaps(); booths.forEach(b=>paintBooth(c,b,X(b.x),Y(b.y),b.w*sc,b.h*sc,sc,{conflict:ov.has(b.id),shopper:mode==='shopper'})); paintCompass(c,cW-52,52,30,northDeg); return cn; } function printDoc(html){ try{ const w=window.open('','_blank'); if(w){ w.document.write(html); w.document.close(); w.focus(); return; } }catch(e){} let f=document.getElementById('printFrame'); if(f)f.remove(); f=document.createElement('iframe'); f.id='printFrame'; f.style.cssText='position:fixed;right:0;bottom:0;width:1px;height:1px;border:0;opacity:0'; document.body.appendChild(f); const doc=f.contentWindow.document; doc.open(); doc.write(html); doc.close(); const go=()=>{ try{ f.contentWindow.focus(); f.contentWindow.print(); }catch(e){} }; f.onload=go; setTimeout(go,500); } function printMap(){ const cn=buildExport(); if(!cn){alert('Add some booths first.');return;} const url=cn.toDataURL('image/png'); const total=booths.length,sold=booths.filter(b=>bStatus(b)!=='available').length,pct=total?Math.round(sold/total*100):0; printDoc(''+esc(curDate().label)+'

'+esc(curDate().label)+' — Market Layout

'+total+' booths · '+pct+'% sold · north '+Math.round(northDeg)+'° · '+new Date().toLocaleString()+'
AvailableReservedOccupied
'); } // ---- CSV import / export ---- function parseCSV(text){ const rows=[]; let i=0,f='',row=[],q=false; text=text.replace(/\r\n/g,'\n').replace(/\r/g,'\n'); while(ir.some(x=>x.trim()!=='')); } function snapCat(s){ if(!s)return 'Other'; const t=s.trim().toLowerCase(); if(!t)return 'Other'; const hit=CATS.find(c=>c.toLowerCase()===t); if(hit)return hit; const part=CATS.find(c=>c.toLowerCase().includes(t)||t.includes(c.toLowerCase())); return part||'Other'; } function importVendorsCSV(text){ const rows=parseCSV(text); if(!rows.length){alert('That CSV looks empty.');return;} let head=rows[0].map(h=>h.trim().toLowerCase()); let start=1; const findCol=(...names)=>head.findIndex(h=>names.some(n=>h===n||h.includes(n))); let iName=findCol('name','vendor','business'), iCat=findCol('category','cat','type'), iEmail=findCol('email','e-mail'), iPhone=findCol('phone','tel','mobile'); if(iName<0){ iName=0; iCat=rows[0].length>1?1:-1; iEmail=-1; iPhone=-1; start=0; } const exist=new Set(vendors.map(v=>(v.name||'').trim().toLowerCase())); let added=0,skipped=0; for(let r=start;r=0?snapCat(row[iCat]):'Other', {email:iEmail>=0?(row[iEmail]||'').trim():'', phone:iPhone>=0?(row[iPhone]||'').trim():''}); exist.add(name.toLowerCase()); added++; } commit(); setTab('vendors'); alert('Imported '+added+' vendor'+(added===1?'':'s')+(skipped?(' · skipped '+skipped+' duplicate'+(skipped===1?'':'s')):'')+'.'); } function exportAssignCSV(){ if(!booths.length){alert('Add some booths first.');return;} const q=v=>{v=String(v==null?'':v); return /[",\n]/.test(v)?'"'+v.replace(/"/g,'""')+'"':v;}; const head=['Booth','Custom name','Size','Zone','Status','Vendor','Category','Email','Phone','Base fee','Amenities','Total fee']; const lines=[head.join(',')]; booths.slice().sort((a,b)=>(a.y-b.y)||(a.x-b.x)).forEach(b=>{ const v=bVendor(b),z=zoneOf(b),am=b.amen||{}; const amen=[am.power?'Power':null,am.water?'Water':null,am.corner?'Corner':null].filter(Boolean).join(' / '); lines.push([b.label,b.name||'',b.sizeKey||(b.w+'x'+b.h),z?z.name:'',bStatus(b),v?v.name:'',v?(v.category||''):'',v?(v.email||''):'',v?(v.phone||''):'',b.fee||0,amen,bFee(b)].map(q).join(',')); }); const blob=new Blob([lines.join('\n')],{type:'text/csv'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=(curDate().label||'market').replace(/[^\w]+/g,'_')+'_booths.csv'; a.click(); } // ---- sample markets (real prospects) ---- function loadSample(kind){ if((booths.length||vendors.length) && !confirm('Replace the current market with this sample layout?'))return; booths=[];zones=[];vendors=[];boothSeq=0;checked.clear();entrances=[];northDeg=0; const day={id:uid(),label:'Market Day 1',assign:{}}; dates=[day]; activeDate=day.id; const mk=(x,y,sz)=>{const s=SIZES[sz]||SIZES.Medium;boothSeq++;const b={id:uid(),x,y,w:s.w,h:s.h,sizeKey:sz,label:String(boothSeq),fee:s.fee,amen:{},lockVendor:'',name:'',logo:''};booths.push(b);day.assign[b.id]={vendorId:'',status:'available',checkedIn:false,noShow:false,walkup:false};return b;}; const seat=(b,v)=>{if(b&&v){day.assign[b.id].vendorId=v.id;day.assign[b.id].status='occupied';}}; const presets={ flea:()=>{ surface='asphalt'; day.label='Salem Flea Market — Saturday'; ['Granite State Pickers','Yankee Treasures','Re-Tooled Tools','Vintage Vinyl Vault','Coin & Card Corner','Second Life Furniture','The Comic Cellar','Estate Finds Co.'].forEach(n=>addVendor(n,'Other')); const cw=11,ch=11,made=[]; for(let blk=0;blk<2;blk++){const ox=blk*(6*cw+16); for(let r=0;r<7;r++)for(let c=0;c<6;c++)made.push(mk(ox+c*cw,r*ch,'Small')); zones.push({id:uid(),x:ox-2,y:-2,w:6*cw,h:7*ch,name:blk?'Hall B':'Hall A',color:ZONE_COLORS[blk?2:0]});} entrances.push({id:uid(),x:6*cw+6,y:7*ch+8}); vendors.forEach((v,i)=>seat(made[i*3],v)); }, farm:()=>{ surface='concrete'; day.label='United Farmers Market — Saturday'; [['Sunny Acres Farm','Produce'],['Rise & Crumb Bakery','Bakery'],['Hilltop Dairy','Dairy'],['Pemaquid Oyster Co.','Meat'],['Smoke & Oak BBQ','Prepared Food'],['Bog Brook Coffee','Beverages'],['Penobscot Pottery','Crafts'],['Wild Cove Flowers','Flowers'],['Two Maples Maple','Other'],['Tide & Thyme','Prepared Food']].forEach(a=>addVendor(a[0],a[1])); const M=SIZES.Medium,cw=M.w+4,made=[]; for(let i=0;i<10;i++){made.push(mk(i*cw,0,'Medium'));made.push(mk(i*cw,M.h+16,'Medium'));} zones.push({id:uid(),x:-2,y:M.h+2,w:10*cw,h:12,name:'Main Aisle',color:ZONE_COLORS[2]}); entrances.push({id:uid(),x:0,y:-8}); entrances.push({id:uid(),x:(10*cw)-cw,y:2*M.h+24}); vendors.forEach((v,i)=>seat(made[i],v)); }, fair:()=>{ surface='grass'; day.label='Deerfield Fair — Day 1'; [['Pine State Fried Dough','Prepared Food'],['County Fair Lemonade','Beverages'],['Maple Ridge Kettle Corn','Prepared Food'],['Granite Grill Truck','Prepared Food'],['North Woods Crafts','Crafts'],['4-H Plant Sale','Flowers'],['Mountain Maple Co.','Other'],['Sap House Cider','Beverages']].forEach(a=>addVendor(a[0],a[1])); const made=[]; for(let i=0;i<8;i++)made.push(mk(i*14,0,'Large')); zones.push({id:uid(),x:-2,y:34,w:6*16,h:42,name:'Food Court',color:ZONE_COLORS[1]}); for(let i=0;i<5;i++)made.push(mk(2+i*16,40,'Food Truck')); for(let i=0;i<10;i++)made.push(mk(i*11,92,'Small')); zones.push({id:uid(),x:-2,y:90,w:10*11,h:12,name:'Crafters Mall',color:ZONE_COLORS[5]}); entrances.push({id:uid(),x:0,y:-10}); entrances.push({id:uid(),x:60,y:122}); [8,9,10,11,18,19,20,21].forEach((idx,k)=>seat(made[idx],vendors[k])); }, craft:()=>{ surface='grass'; day.label='Gunstock Craft Fair — Saturday'; ['Birch & Bramble','Knotty Pine Woodworks','Lakes Region Candles','Stitch & Sparrow','White Mtn Soap Co.','Harbor Glass Studio','Fern & Feather','Granite Knits'].forEach(n=>addVendor(n,'Crafts')); const cw=11,ch=11,made=[]; for(let i=0;i<9;i++){made.push(mk(i*cw,0,'Small'));made.push(mk(i*cw,5*ch,'Small'));} for(let r=1;r<5;r++){made.push(mk(0,r*ch,'Small'));made.push(mk(8*cw,r*ch,'Small'));} zones.push({id:uid(),x:cw,y:ch,w:6*cw,h:3*ch,name:'Demo Stage',color:ZONE_COLORS[3]}); entrances.push({id:uid(),x:4*cw,y:-6}); vendors.forEach((v,i)=>seat(made[i*2],v)); } }; (presets[kind]||presets.farm)(); const su=document.getElementById('surface'); if(su)su.value=surface; sel=null;closeSheet();renderDates();fit();renderSide(); history=[snapshot()];hpos=0;updateHistBtns();autosave(); } // ---- toolbar wiring ---- document.getElementById('modeSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(b)setMode(b.dataset.mode);}); document.getElementById('tabs').addEventListener('click',e=>{const b=e.target.closest('button');if(b)setTab(b.dataset.tab);}); document.getElementById('addSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;const[wx,wy]=viewCenterWorld();addBooth(b.dataset.size,wx,wy);}); document.getElementById('addZone').onclick=()=>{const[wx,wy]=viewCenterWorld();addZoneAt(wx,wy);}; document.getElementById('tpl').onchange=e=>{if(e.target.value){template(e.target.value);e.target.value='';}}; document.getElementById('autopack').onclick=()=>autoPack(); document.getElementById('autoseat').onclick=autoSeat; document.getElementById('addEntrance').onclick=()=>{placingEntrance=!placingEntrance;document.getElementById('addEntrance').classList.toggle('on',placingEntrance);}; document.getElementById('heatBtn').onclick=()=>{heatOn=!heatOn;document.getElementById('heatBtn').classList.toggle('on',heatOn);draw();}; document.getElementById('optimizeBtn').onclick=pricingSuggest; document.getElementById('shareBtn').onclick=sharePanel; document.getElementById('passBtn').onclick=()=>vendorPass(vendorViewId); document.getElementById('vendorPick').onchange=e=>{vendorViewId=e.target.value;renderVendorView();draw();}; document.getElementById('surface').onchange=e=>{surface=e.target.value;commit();}; document.getElementById('multiBtn').onclick=e=>{multiOn=!multiOn;e.target.classList.toggle('on',multiOn);}; document.getElementById('snapBtn').onclick=e=>{snap=snap?0:1;e.target.textContent='Snap'+(snap?'':' off');e.target.classList.toggle('on',!!snap);}; document.getElementById('checkinBtn').onclick=e=>{checkinMode=!checkinMode;multiOn=false;document.getElementById('multiBtn').classList.remove('on');e.target.classList.toggle('on',checkinMode);sel=null;closeSheet();refresh();}; document.getElementById('undoBtn').onclick=undo; document.getElementById('redoBtn').onclick=redo; document.getElementById('reportBtn').onclick=revenueReport; document.getElementById('printBtn').onclick=printMap; document.getElementById('addDate').onclick=addDate; document.getElementById('dateSel').onchange=e=>{activeDate=e.target.value;sel=null;closeSheet();refresh();}; document.getElementById('clearBtn').onclick=()=>{if(confirm('Clear booths & zones for ALL dates?')){booths=[];zones=[];boothSeq=0;dates.forEach(d=>d.assign={});sel=null;checked.clear();closeSheet();commit();}}; document.getElementById('zin').onclick=()=>zoomBtn(1.2); document.getElementById('zout').onclick=()=>zoomBtn(1/1.2); document.getElementById('zfit').onclick=fit; document.getElementById('exportBtn').onclick=()=>{const blob=new Blob([snapshot()],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='market-layout.json';a.click();}; document.getElementById('importBtn').onclick=()=>document.getElementById('importFile').click(); document.getElementById('importFile').onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=()=>{try{applySnapshot(r.result);sel=null;checked.clear();closeSheet();renderDates();fit();renderSide();history=[snapshot()];hpos=0;updateHistBtns();}catch(err){alert('Could not read that file.');}};r.readAsText(f);}; document.getElementById('impVendBtn').onclick=()=>document.getElementById('vendCsvFile').click(); document.getElementById('vendCsvFile').onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=()=>{try{importVendorsCSV(r.result);}catch(err){alert('Could not parse that CSV.');}};r.readAsText(f);e.target.value='';}; document.getElementById('expCsvBtn').onclick=exportAssignCSV; document.getElementById('sampleSel').onchange=e=>{if(e.target.value){loadSample(e.target.value);e.target.value='';}}; document.getElementById('filters').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;filter=b.dataset.f;document.querySelectorAll('#filters button').forEach(x=>x.classList.toggle('on',x===b));refresh();}); document.getElementById('selAll').onchange=e=>{const vis=visibleBooths();if(e.target.checked)vis.forEach(b=>checked.add(b.id));else vis.forEach(b=>checked.delete(b.id));refresh();}; document.getElementById('bulkApply').onclick=()=>{const base=document.getElementById('bulkName').value,numbered=document.getElementById('bulkNumber').checked,s=selectedBooths();if(!s.length){alert('Select booths first.');return;}s.forEach((b,i)=>{b.name=numbered?(base?base+' '+(i+1):String(i+1)):base;});commit();}; function bulkStatus(s){const a=selectedBooths();if(!a.length){alert('Select booths first.');return;}a.forEach(b=>ensureA(b).status=s);commit();} document.getElementById('bsAv').onclick=()=>bulkStatus('available');document.getElementById('bsRe').onclick=()=>bulkStatus('reserved');document.getElementById('bsOc').onclick=()=>bulkStatus('occupied'); document.querySelectorAll('#bulk .agrid button').forEach(b=>b.onclick=()=>align(b.dataset.al)); document.getElementById('bCopy').onclick=doCopy; document.getElementById('bPaste').onclick=doPaste; document.getElementById('bDel').onclick=()=>{if(!checked.size){alert('Select booths first.');return;}if(!confirm('Delete '+checked.size+' booth(s)?'))return;booths=booths.filter(b=>!checked.has(b.id));dates.forEach(d=>checked.forEach(id=>delete d.assign[id]));checked.clear();sel=null;closeSheet();commit();}; document.getElementById('addVendorBtn').onclick=()=>{const i=document.getElementById('newVendor');const n=i.value.trim();if(!n)return;const v=addVendor(n,'Other');i.value='';commit();openVendorEditor(v.id);}; document.getElementById('newVendor').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('addVendorBtn').click();}); document.getElementById('search').addEventListener('input',e=>{searchTerm=e.target.value;renderShop();draw();}); document.getElementById('modal').addEventListener('click',e=>{if(e.target.id==='modal')e.target.classList.remove('show');}); const side=document.getElementById('side'); document.getElementById('sideToggle').onclick=()=>side.classList.toggle('open'); document.getElementById('sideClose').onclick=()=>side.classList.remove('open'); document.addEventListener('keydown',e=>{ const t=document.activeElement; if(t&&/INPUT|SELECT|TEXTAREA/.test(t.tagName))return; const ctrl=e.ctrlKey||e.metaKey; if(ctrl&&e.key.toLowerCase()==='z'){e.preventDefault();e.shiftKey?redo():undo();} else if(ctrl&&e.key.toLowerCase()==='y'){e.preventDefault();redo();} else if(ctrl&&e.key.toLowerCase()==='c'){doCopy();} else if(ctrl&&e.key.toLowerCase()==='v'){e.preventDefault();doPaste();} else if(e.key==='Escape'){if(armVendor){armVendor=null;updateBanner();renderSide();}} else if(e.key==='Delete'||e.key==='Backspace'){ if(checked.size){booths=booths.filter(b=>!checked.has(b.id));dates.forEach(d=>checked.forEach(id=>delete d.assign[id]));checked.clear();sel=null;closeSheet();commit();} else if(sel){const o=selObj();if(o){if(sel.type==='booth'){booths=booths.filter(b=>b.id!==o.id);dates.forEach(d=>delete d.assign[o.id]);}else zones=zones.filter(z=>z.id!==o.id);sel=null;closeSheet();commit();}} } }); // ---- init ---- window.addEventListener('resize',resize); resize(); let loaded=false; try{const s=localStorage.getItem('vb_state'); if(s){applySnapshot(s);loaded=true;}}catch(e){} if(loaded){ renderDates(); fit(); renderSide(); history=[snapshot()]; hpos=0; updateHistBtns(); } else { dates=[{id:uid(),label:'Market Day 1',assign:{}}]; activeDate=dates[0].id; // seed vendors + double-row template ['Sunny Acres Farm/Produce','Rise & Crumb Bakery/Bakery','Hilltop Dairy/Dairy','Smoke & Oak BBQ/Prepared Food','Bee Happy Honey/Other','Petal Pushers/Flowers'].forEach(s=>{const[n,c]=s.split('/');addVendor(n,c);}); renderDates(); template('double'); // seat a few demo vendors on day 1 booths.slice(0,vendors.length).forEach((b,i)=>{const a=ensureA(b);a.vendorId=vendors[i].id;a.status='occupied';}); pushHistory(); refresh(); } try{ const q=new URLSearchParams(location.search); const vw=q.get('view'); if(vw==='shopper'||vw==='vendor'){ const vid=q.get('v'); if(vid)vendorViewId=vid; document.body.classList.add('kiosk'); setMode(vw); } }catch(e){} })();