datewise/prototype/datewise-prototype.html
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

775 lines
93 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<title>Datewise Prototype</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
/* Layout: app bar with view switch + simulated clock; two working surfaces (storefront, admin) built from one token set. */
:root{
--bg:#F2F5F3; --surface:#FFFFFF; --sunk:#E8EEEB; --ink:#13201D; --muted:#5A6965; --line:#D6DFDB;
--accent:#0D6B5D; --accent-ink:#FFFFFF; --accent-soft:#DDEFEA;
--peak:#A86400; --peak-soft:#FFF1D6;
--ok:#1E7A3A; --ok-soft:#E1F3E5; --bad:#B42318; --bad-soft:#FBE4E1; --warn:#9A5B00; --warn-soft:#FDF0D8;
--display:"Bricolage Grotesque", "Segoe UI", system-ui, sans-serif;
--body:"IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
--mono:"IBM Plex Mono", ui-monospace, Menlo, Consolas, monospace;
--r:10px;
}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
--bg:#0D1412; --surface:#151F1C; --sunk:#1B2824; --ink:#E4EDEA; --muted:#97A8A3; --line:#2A3A35;
--accent:#4CC2AD; --accent-ink:#04201A; --accent-soft:#163A33;
--peak:#F0B44A; --peak-soft:#3A2B0F;
--ok:#5FCB7E; --ok-soft:#173523; --bad:#F2877A; --bad-soft:#3E1C18; --warn:#F0B44A; --warn-soft:#3A2B0F;
color-scheme:dark}}
:root[data-theme="dark"]{
--bg:#0D1412; --surface:#151F1C; --sunk:#1B2824; --ink:#E4EDEA; --muted:#97A8A3; --line:#2A3A35;
--accent:#4CC2AD; --accent-ink:#04201A; --accent-soft:#163A33;
--peak:#F0B44A; --peak-soft:#3A2B0F;
--ok:#5FCB7E; --ok-soft:#173523; --bad:#F2877A; --bad-soft:#3E1C18; --warn:#F0B44A; --warn-soft:#3A2B0F;
color-scheme:dark}
*{box-sizing:border-box}
body{background:var(--bg);color:var(--ink);font:15px/1.5 var(--body);margin:0}
button,input,select,textarea{font:inherit;color:inherit}
button{cursor:pointer}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
h1,h2,h3{font-family:var(--display);text-wrap:balance;margin:0;letter-spacing:-.01em}
h2{font-size:1.25rem} h3{font-size:1.02rem}
.mono,.num{font-family:var(--mono);font-variant-numeric:tabular-nums}
.muted{color:var(--muted)} .small{font-size:.85rem}
.label{font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:600}
/* App bar */
.bar{position:sticky;top:env(safe-area-inset-top,0px);z-index:5;background:var(--surface);border-bottom:1px solid var(--line)}
.bar-in{max-width:1240px;margin:0 auto;padding:10px 16px;display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center}
.brand{display:flex;align-items:center;gap:10px}
.brand svg{flex:none}
.brand b{font-family:var(--display);font-size:1.2rem}
.brand span{font-size:.75rem;color:var(--muted);border:1px solid var(--line);border-radius:99px;padding:1px 8px}
.seg{display:inline-flex;background:var(--sunk);border-radius:99px;padding:3px}
.seg button{border:0;background:transparent;padding:6px 14px;border-radius:99px;font-weight:500;color:var(--muted)}
.seg button[aria-pressed="true"]{background:var(--surface);color:var(--ink);box-shadow:0 1px 2px rgb(0 0 0/.12)}
.clock{margin-left:auto;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.clock .now{font-family:var(--mono);font-size:.86rem;background:var(--sunk);padding:5px 10px;border-radius:8px}
select,input[type=text],input[type=date],input[type=number],input[type=email],input[type=tel],input[type=time]{
background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:6px 9px;min-width:0}
.btn{border:1px solid var(--line);background:var(--surface);border-radius:8px;padding:7px 12px;font-weight:500}
.btn:hover{border-color:var(--accent)}
.btn.primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.btn.primary:disabled{opacity:.45;cursor:not-allowed}
a.btn{text-decoration:none;color:var(--ink);display:inline-block}
.btn.ghost{border-color:transparent;background:transparent;color:var(--accent);padding:4px 6px}
.btn.sm{padding:4px 9px;font-size:.85rem}
main{max-width:1240px;margin:0 auto;padding-inline:16px;padding-block:20px 60px}
.notice{border-radius:var(--r);padding:10px 14px;margin-bottom:14px;display:flex;gap:10px;align-items:flex-start}
.notice.bad{background:var(--bad-soft);color:var(--bad)}
.notice.ok{background:var(--ok-soft);color:var(--ok)}
.notice.peak{background:var(--peak-soft);color:var(--peak)}
.notice.info{background:var(--accent-soft);color:var(--ink)}
/* Storefront */
.store{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);gap:22px;align-items:start}
@media (max-width:900px){.store{grid-template-columns:1fr}}
.panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:18px;min-width:0}
.storehead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;gap:10px;flex-wrap:wrap}
.item{display:grid;grid-template-columns:44px minmax(0,1fr) auto;gap:12px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}
.item:first-of-type{border-top:0}
.thumb{width:44px;height:44px;border-radius:9px;display:grid;place-items:center;font-family:var(--display);font-weight:700;color:var(--accent-ink)}
.item .t{font-weight:500}
.tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}
.tag{font-size:.72rem;border-radius:99px;padding:1px 8px;background:var(--sunk);color:var(--muted)}
.tag.fresh{background:var(--ok-soft);color:var(--ok)}
.qty{display:flex;align-items:center;gap:4px}
.qty button{width:28px;height:28px;border-radius:7px;border:1px solid var(--line);background:var(--surface)}
.qty span{min-width:22px;text-align:center;font-family:var(--mono)}
.totals{display:grid;grid-template-columns:1fr auto;gap:4px 12px;margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}
.totals .grand{font-weight:600;font-size:1.05rem}
.widget h2{display:flex;align-items:center;gap:8px}
.methods{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:14px 0}
.mbtn{border:1.5px solid var(--line);background:var(--surface);border-radius:11px;padding:10px;text-align:left;display:grid;gap:2px}
.mbtn b{font-weight:600}
.mbtn small{color:var(--muted);font-size:.78rem}
.mbtn[aria-pressed="true"]{border-color:var(--accent);background:var(--accent-soft)}
.mbtn:disabled{opacity:.55;cursor:not-allowed}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.field{display:grid;gap:4px;min-width:0}
.loclist{display:grid;gap:6px;margin-top:6px}
.loc{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:center;border:1px solid var(--line);border-radius:10px;padding:8px 10px;background:var(--surface);text-align:left}
.loc[aria-pressed="true"]{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.loc:disabled{cursor:not-allowed;background:var(--sunk)}
.dot{width:9px;height:9px;border-radius:50%;background:var(--ok)}
.loc:disabled .dot{background:var(--muted)}
.cal{margin-top:14px}
.calhead{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
.grid7{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:4px}
.dow{font-size:.72rem;text-align:center;color:var(--muted);font-weight:600;padding-bottom:2px}
.day{position:relative;aspect-ratio:1/0.86;border-radius:9px;border:1px solid transparent;background:transparent;display:grid;place-items:center;font-family:var(--mono);font-size:.92rem;padding:0;max-width:100%}
.day.avail{background:var(--accent-soft);border-color:transparent}
.day.avail:hover{border-color:var(--accent)}
.day.full{background:var(--warn-soft);color:var(--warn)}
.day.closed{color:var(--muted);text-decoration:line-through;text-decoration-color:var(--line)}
.day.out{color:var(--line);cursor:default}
.day.sel{background:var(--accent);color:var(--accent-ink)}
.day .pk{position:absolute;top:4px;right:5px;width:6px;height:6px;border-radius:50%;background:var(--peak)}
.day .lft{position:absolute;bottom:2px;font-size:.6rem;font-family:var(--body);opacity:.8}
.legend{display:flex;gap:12px;flex-wrap:wrap;font-size:.76rem;color:var(--muted);margin-top:8px}
.legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px;vertical-align:-1px}
.slots{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:10px}
.slot{border:1.5px solid var(--line);border-radius:10px;padding:8px 10px;background:var(--surface);text-align:left;display:grid;gap:4px}
.slot b{font-family:var(--mono);font-weight:500;font-size:.9rem}
.slot[aria-pressed="true"]{border-color:var(--accent);background:var(--accent-soft)}
.slot:disabled{background:var(--sunk);cursor:not-allowed;color:var(--muted)}
.meter{height:5px;border-radius:9px;background:var(--sunk);overflow:hidden}
.meter i{display:block;height:100%;background:var(--accent)}
.meter.low i{background:var(--warn)}
.slot small{font-size:.74rem;color:var(--muted)}
.hold{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;background:var(--sunk);border-radius:10px;padding:9px 12px;margin-top:12px}
.hold .mono{font-size:1rem}
.summary{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 14px;margin-top:12px;font-size:.92rem}
.summary dt{color:var(--muted)} .summary dd{margin:0}
.demo{border:1px dashed var(--line);border-radius:10px;padding:10px 12px;margin-top:14px;display:grid;gap:8px}
.co{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:12px}
@media (max-width:560px){.co{grid-template-columns:1fr}.methods{grid-template-columns:1fr}}
.confirm{display:grid;gap:12px}
.timeline{display:grid;gap:6px}
.timeline div{display:grid;grid-template-columns:150px minmax(0,1fr);gap:10px;font-size:.88rem}
/* Admin */
.admin{display:grid;grid-template-columns:200px minmax(0,1fr);gap:22px;align-items:start}
@media (max-width:820px){.admin{grid-template-columns:1fr}}
.nav{display:grid;gap:2px;position:sticky;top:76px}
@media (max-width:820px){.nav{position:static;grid-auto-flow:column;overflow-x:auto;grid-auto-columns:max-content}}
.nav button{border:0;background:transparent;text-align:left;padding:8px 12px;border-radius:8px;color:var(--muted);font-weight:500;display:flex;justify-content:space-between;gap:10px}
.nav button[aria-current="true"]{background:var(--surface);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent)}
.badge{font-size:.72rem;border-radius:99px;padding:0 7px;background:var(--bad-soft);color:var(--bad);font-family:var(--mono)}
.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
@media (max-width:700px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}
.kpi{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px 14px}
.kpi .v{font-family:var(--display);font-size:1.7rem;font-weight:700;line-height:1.1}
.kpi.alert .v{color:var(--bad)}
.stack{display:grid;gap:16px}
.capchart{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(26px,1fr);gap:4px;align-items:end;height:150px;padding-top:8px;overflow-x:auto}
.cbar{display:grid;grid-template-rows:1fr auto auto;height:100%;gap:3px;text-align:center}
.cbar .track{position:relative;background:var(--sunk);border-radius:5px;overflow:hidden;align-self:stretch}
.cbar .fill{position:absolute;left:0;right:0;bottom:0;background:var(--accent)}
.cbar.peak .fill{background:var(--peak)}
.cbar.full .fill{background:var(--bad)}
.cbar span{font-size:.64rem;color:var(--muted);font-family:var(--mono)}
.tablewrap{overflow-x:auto;border:1px solid var(--line);border-radius:12px;background:var(--surface)}
table{border-collapse:collapse;width:100%;font-size:.88rem}
th{font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);text-align:left;font-weight:600;background:var(--sunk)}
th,td{padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;vertical-align:middle}
tr:last-child td{border-bottom:0}
td.wrap{white-space:normal;min-width:180px}
.pill{font-size:.74rem;padding:1px 8px;border-radius:99px;font-weight:500;display:inline-block}
.pill.confirmed{background:var(--ok-soft);color:var(--ok)}
.pill.needs_review{background:var(--bad-soft);color:var(--bad)}
.pill.cancelled{background:var(--sunk);color:var(--muted)}
.pill.peak{background:var(--peak-soft);color:var(--peak)}
.filters{display:flex;flex-wrap:wrap;gap:8px;align-items:end;margin-bottom:10px}
.form{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px}
.chips{display:flex;flex-wrap:wrap;gap:6px}
.chip{display:inline-flex;gap:6px;align-items:center;background:var(--sunk);border-radius:99px;padding:3px 6px 3px 10px;font-family:var(--mono);font-size:.82rem}
.chip button{border:0;background:transparent;color:var(--muted);padding:0 4px}
.strip{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(30px,1fr);gap:3px;overflow-x:auto}
.strip div{border-radius:6px;padding:4px 0;text-align:center;font-size:.68rem;font-family:var(--mono)}
.strip .y{background:var(--ok-soft);color:var(--ok)} .strip .n{background:var(--sunk);color:var(--muted)}
.reasons{display:grid;gap:6px}
.reason{display:grid;grid-template-columns:120px minmax(0,1fr);gap:10px;padding:8px 10px;border-radius:8px;background:var(--sunk);font-size:.88rem}
.reason.ok{background:var(--ok-soft)}
code{font-family:var(--mono);font-size:.82rem;background:var(--sunk);padding:1px 5px;border-radius:5px}
textarea{width:100%;min-height:120px;border:1px solid var(--line);border-radius:8px;padding:8px;font-family:var(--mono);font-size:.78rem;background:var(--surface)}
.switch{display:inline-flex;gap:8px;align-items:center}
.sectionhead{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}
</style>
<header class="bar"><div class="bar-in">
<div class="brand">
<svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect x="2" y="4" width="24" height="21" rx="5" fill="var(--accent)"/><rect x="2" y="4" width="24" height="6" rx="3" fill="var(--ink)" opacity=".25"/><path d="M8.5 16.5l3.6 3.4 7.4-7.4" stroke="var(--accent-ink)" stroke-width="2.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<b>Datewise</b><span>prototype · sample data</span>
</div>
<div class="seg" role="group" aria-label="View">
<button id="v-shop" data-act="view" data-v="shop">Customer view</button>
<button id="v-admin" data-act="view" data-v="admin">Merchant admin</button>
</div>
<div class="clock">
<span class="label">Store time</span>
<span class="now" id="nowLabel"></span>
<select id="preset" aria-label="Jump to a moment">
<option value="live">Live clock</option>
<option value="2026-10-14T09:00">Wed Oct 14, 9:00 AM</option>
<option value="2026-10-14T14:05">Wed Oct 14, 2:05 PM: after pickup cutoff</option>
<option value="2026-11-05T10:00">Thu Nov 5, 10:00 AM: Diwali week</option>
<option value="2026-11-06T18:01">Fri Nov 6, 6:01 PM: Diwali cutoff passed</option>
</select>
<button class="btn sm" data-act="skip">+11 min</button>
</div>
</div></header>
<main id="app"></main>
<script>"use strict";var DatewiseEngine=(()=>{var K=Object.defineProperty;var Ue=Object.getOwnPropertyDescriptor;var $e=Object.getOwnPropertyNames;var Be=Object.prototype.hasOwnProperty;var Fe=(e,t,n)=>t in e?K(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var Ne=(e,t)=>{for(var n in t)K(e,n,{get:t[n],enumerable:!0})},We=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of $e(t))!Be.call(e,r)&&r!==n&&K(e,r,{get:()=>t[r],enumerable:!(o=Ue(t,r))||o.enumerable});return e};var Ke=e=>We(K({},"__esModule",{value:!0}),e);var O=(e,t,n)=>Fe(e,typeof t!="symbol"?t+"":t,n);var Je={};Ne(Je,{METHODS:()=>H,MemoryReservationStore:()=>se,addDays:()=>I,addOperatingMinutes:()=>Oe,buildRunSheet:()=>Xe,canCustomerReschedule:()=>Ve,capsFor:()=>ze,checkFreshness:()=>G,computeAvailability:()=>V,diffDays:()=>Ye,evaluateRules:()=>F,firstAvailable:()=>ne,fromMinutes:()=>Y,haversineKm:()=>Z,hoursFor:()=>te,iso:()=>B,itemMatches:()=>Te,localParts:()=>L,mapsUrls:()=>Ee,matchZone:()=>J,normalizePostal:()=>q,peakFor:()=>j,planReminders:()=>Qe,postalMatches:()=>Me,quoteFee:()=>ee,rankLocations:()=>je,renderTemplate:()=>Le,slotId:()=>we,slotStartMs:()=>re,stockedAt:()=>oe,toCsv:()=>Ge,toMinutes:()=>D,tzOffsetMinutes:()=>X,usageKey:()=>z,validateSelection:()=>Ze,weekdayOf:()=>$,zonedToUtc:()=>v});var H=["pickup","delivery","shipping"];var De=new Map;function Ie(e){let t=De.get(e);return t||(t=new Intl.DateTimeFormat("en-US",{timeZone:e,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",weekday:"short"}),De.set(e,t)),t}var He={Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6};function L(e,t){let n={};for(let r of Ie(t).formatToParts(new Date(e)))n[r.type]=r.value;let o=n.hour==="24"?"00":n.hour;return{date:`${n.year}-${n.month}-${n.day}`,time:`${o}:${n.minute}`,minutes:Number(o)*60+Number(n.minute),weekday:He[n.weekday]}}function X(e,t){let n={};for(let i of Ie(t).formatToParts(new Date(e)))n[i.type]=i.value;let o=n.hour==="24"?0:Number(n.hour),r=Date.UTC(+n.year,+n.month-1,+n.day,o,+n.minute,+n.second);return Math.round((r-Math.floor(e/1e3)*1e3)/6e4)}function v(e,t,n){let[o,r,i]=e.split("-").map(Number),[s,a]=t.split(":").map(Number),u=Date.UTC(o,r-1,i,s,a),d=X(u,n),l=u-d*6e4,c=X(l,n);if(c!==d){let g=u-c*6e4;L(g,n).time===t?l=Math.min(l,g):L(l,n).time!==t&&(l=Math.max(l,g))}return l}function D(e){let[t,n]=e.split(":").map(Number);return t*60+n}function Y(e){let t=Math.floor(e/60),n=e%60;return`${String(t).padStart(2,"0")}:${String(n).padStart(2,"0")}`}function I(e,t){let[n,o,r]=e.split("-").map(Number);return new Date(Date.UTC(n,o-1,r+t)).toISOString().slice(0,10)}function Ye(e,t){let[n,o,r]=e.split("-").map(Number),[i,s,a]=t.split("-").map(Number);return Math.round((Date.UTC(n,o-1,r)-Date.UTC(i,s-1,a))/864e5)}function $(e){let[t,n,o]=e.split("-").map(Number);return new Date(Date.UTC(t,n-1,o)).getUTCDay()}function B(e){return new Date(e).toISOString()}function Te(e,t){var r,i;let n=t.target,o=n.values.map(s=>String(s).toLowerCase());switch(n.type){case"product":return o.includes(String(e.productId).toLowerCase());case"variant":return o.includes(String(e.variantId).toLowerCase());case"collection":return((r=e.collectionIds)!=null?r:[]).some(s=>o.includes(String(s).toLowerCase()));case"vendor":return!!e.vendor&&o.includes(e.vendor.toLowerCase());case"productType":return!!e.productType&&o.includes(e.productType.toLowerCase());case"tag":return((i=e.tags)!=null?i:[]).some(s=>o.includes(s.toLowerCase()))}}function E(e,t,n){return{code:"BLOCKED_BY_PRODUCT",message:e.message||n.replace("{item}",t.title),ruleId:e.id}}function F(e,t,n="longest"){var a,u,d,l;let o={allowedMethods:new Set(H),methodReasons:new Map,allowedLocations:null,locationReasons:[],extraPrepMinutes:0,blockedWeekdays:new Map,blockedDates:new Map,windows:[],freshness:[],requiresScheduling:!1,matched:[]},r=t.filter(c=>c.enabled).sort((c,g)=>c.priority-g.priority),i=[],s=0;for(let c of e.items){let g=0,y;for(let m of r){if(!Te(c,m))continue;o.matched.push({ruleId:m.id,ruleName:m.name,itemTitle:c.title});let f=m.action;switch(f.type){case"allowOnlyMethods":for(let h of H)!f.methods.includes(h)&&o.allowedMethods.has(h)&&(o.allowedMethods.delete(h),ve(o.methodReasons,h,{...E(m,c,`{item} is only available for ${f.methods.join(" or ")}`),code:"METHOD_PROHIBITED_BY_PRODUCT"}));break;case"prohibitMethods":for(let h of f.methods)i.push({m:h,reason:{...E(m,c,`{item} can't be sent by ${h}`),code:"METHOD_PROHIBITED_BY_PRODUCT"}});break;case"restrictLocations":{let h=new Set(f.locationIds);o.allowedLocations=o.allowedLocations?new Set([...o.allowedLocations].filter(C=>h.has(C))):h,o.locationReasons.push({...E(m,c,"{item} is only available from selected locations"),code:"LOCATION_PROHIBITED_BY_PRODUCT"});break}case"addPrep":g+=f.minutes,y=m.id;break;case"blockWeekdays":for(let h of f.weekdays)o.blockedWeekdays.has(h)||o.blockedWeekdays.set(h,E(m,c,"{item} isn't available on this day"));break;case"blockDates":for(let h of f.dates)o.blockedDates.has(h)||o.blockedDates.set(h,E(m,c,"{item} isn't available on this date"));break;case"dateWindow":o.windows.push({from:f.from,to:f.to,reason:E(m,c,"{item} is only available in a limited window")});break;case"freshness":o.freshness.push({ruleId:m.id,itemTitle:c.title,arrivals:f.arrivals,minDaysAfterArrival:f.minDaysAfterArrival,shelfLifeDays:f.shelfLifeDays,message:m.message});break;case"requireScheduling":o.requiresScheduling=!0;break}}n==="sum"&&(s+=g),g>((u=(a=o.prepDriver)==null?void 0:a.minutes)!=null?u:0)&&y&&(o.prepDriver={itemTitle:c.title,ruleId:y,minutes:g})}for(let{m:c,reason:g}of i)o.allowedMethods.delete(c),ve(o.methodReasons,c,g);return o.extraPrepMinutes=n==="sum"?s:(l=(d=o.prepDriver)==null?void 0:d.minutes)!=null?l:0,o}function ve(e,t,n){var r;let o=(r=e.get(t))!=null?r:[];o.some(i=>i.ruleId===n.ruleId&&i.message===n.message)||o.push(n),e.set(t,o)}function G(e,t,n){for(let o of e){let r=o.arrivals.filter(s=>{var a;return!((a=s.locationIds)!=null&&a.length)||s.locationIds.includes(n)});if(!r.length)return{code:"NO_STOCK_ARRIVAL",message:o.message||`${o.itemTitle}: no fresh stock is scheduled for this location yet`,ruleId:o.ruleId};if(!r.some(s=>t>=I(s.date,o.minDaysAfterArrival)&&t<=I(s.date,o.shelfLifeDays))){let s=r.map(a=>I(a.date,o.minDaysAfterArrival)).filter(a=>a>t).sort()[0];return s?{code:"NOT_FRESH_YET",message:o.message||`${o.itemTitle}: next fresh batch is ready from ${s}`,ruleId:o.ruleId}:{code:"PAST_FRESHNESS",message:o.message||`${o.itemTitle}: outside its freshness window on this date`,ruleId:o.ruleId}}}return null}function q(e){return(e||"").toUpperCase().replace(/[\s-]/g,"")}function Me(e,t){let n=q(e),o=t.trim().toUpperCase();if(!n||!o)return!1;if(o.includes("..")){let[i,s]=o.split("..").map(q);if(!i||i.length!==s.length)return!1;let a=n.slice(0,i.length);return a>=i&&a<=s}let r=q(o);return r.endsWith("*")?n.startsWith(r.slice(0,-1)):n===r||r.length===3&&n.startsWith(r)}function Z(e,t,n,o){let i=d=>d*Math.PI/180,s=i(n-e),a=i(o-t),u=Math.sin(s/2)**2+Math.cos(i(e))*Math.cos(i(n))*Math.sin(a/2)**2;return 2*6371*Math.asin(Math.sqrt(u))}function J(e,t){var r,i;let n=e.lat!=null&&e.lng!=null&&t.lat!=null&&t.lng!=null?Z(e.lat,e.lng,t.lat,t.lng):null,o=!1;for(let s of e.zones){if((r=s.postalPatterns)!=null&&r.length&&t.postal&&s.postalPatterns.some(a=>Me(t.postal,a)))return{zone:s,distanceKm:n};if(s.radiusKm!=null&&n!=null&&n<=s.radiusKm)return{zone:s,distanceKm:n};if(s.drivingKm!=null){let a=(i=t.drivingKm)==null?void 0:i[e.id];if(a==null)o=!0;else if(a<=s.drivingKm)return{zone:s,distanceKm:a}}}return{zone:null,distanceKm:n,unverified:o,reason:{code:"OUTSIDE_DELIVERY_AREA",message:o?"We couldn't verify your address right now. Please try pickup or check again shortly.":`${e.name} doesn't deliver to this address yet`}}}function ee(e,t,n=[]){var a,u;let o=(a=e==null?void 0:e.fee)!=null?a:0,r=(e==null?void 0:e.freeOver)!=null&&t>=e.freeOver,i=n.filter(d=>d.amount>0),s=(u=e==null?void 0:e.minOrder)!=null?u:null;return{baseFee:o,waived:r,surcharges:i,fee:(r?0:o)+i.reduce((d,l)=>d+l.amount,0),belowMinimum:s!=null&&t<s,minOrder:s}}var z=(e,t,n)=>`${e}|${t}|${n}`,we=(e,t)=>`${e}-${t}`;function te(e,t,n){var i,s;let o=e.methods[t],r=e.exceptions.find(a=>{var u;return a.date===n&&(!((u=a.methods)!=null&&u.length)||a.methods.includes(t))});return r!=null&&r.closed?{hours:[],exception:{code:"CLOSED_EXCEPTION",message:r.note||"Closed on this date"}}:(i=r==null?void 0:r.hours)!=null&&i.length?{hours:r.hours,special:!0}:{hours:(s=o==null?void 0:o.weeklyHours[$(n)])!=null?s:[]}}function j(e,t,n,o){var r;return(r=e.find(i=>{var s,a;return i.dates.includes(o)&&(!((s=i.locationIds)!=null&&s.length)||i.locationIds.includes(t))&&(!((a=i.methods)!=null&&a.length)||i.methods.includes(n))}))!=null?r:null}function qe(e,t,n,o){var s,a;if(!n.length)return[];if(e.slotMode==="dateOnly")return[{start:n[0].start,end:n[n.length-1].end}];if(e.slotMode==="explicit"&&!o)return((a=(s=e.slots)==null?void 0:s[$(t)])!=null?a:[]).filter(d=>n.some(l=>D(d.start)>=D(l.start)&&D(d.end)<=D(l.end)));let r=e.slotMinutes||60,i=[];for(let u of n)for(let d=D(u.start);d+r<=D(u.end);d+=r)i.push({start:Y(d),end:Y(d+r)});return i}function Oe(e,t,n,o){if(t<=0)return e;let r=t*6e4,i=L(e,n.timezone).date;for(let s=0;s<400;s++,i=I(i,1))for(let a of te(n,o,i).hours){let u=v(i,a.start,n.timezone),d=v(i,a.end,n.timezone);if(d<=e)continue;let l=Math.max(u,e),c=d-l;if(r<=c)return l+r;r-=c}return Number.POSITIVE_INFINITY}function V(e){var f,h,C,M,w,P,N,_,ie,ae,le,de,ue,ce;let{location:t,method:n,now:o,usage:r}=e,i=t.timezone,s=t.methods[n],a=(f=e.constraints)!=null?f:F(e.cart,e.rules,e.settings.mixedPrepPolicy),u=[];t.enabled||u.push({code:"LOCATION_DISABLED",message:`${t.name} isn't taking bookings right now`}),s!=null&&s.enabled||u.push({code:"METHOD_DISABLED",message:`${t.name} doesn't offer ${n}`}),a.allowedMethods.has(n)||u.push(...(h=a.methodReasons.get(n))!=null?h:[]),a.allowedLocations&&!a.allowedLocations.has(t.id)&&u.push(...a.locationReasons);let d=((C=s==null?void 0:s.prepMinutes)!=null?C:0)+a.extraPrepMinutes,l=new Map,c=x=>(l.has(x)||l.set(x,(s==null?void 0:s.prepMode)==="operating"?Oe(o,x,t,n):o+x*6e4),l.get(x)),g={locationId:t.id,method:n,timezone:i,dateOnly:(s==null?void 0:s.slotMode)==="dateOnly",prepMinutes:d,earliestReadyUtc:B(c(d)),blockers:u,dates:[]};if(u.length||!s)return g;let y=L(o,i),m=Math.min(s.horizonDays,(M=e.maxDays)!=null?M:s.horizonDays);for(let x=0;x<m;x++){let R=I(y.date,x),me=$(R),p=j(e.peakDays,t.id,n,R),k={date:R,weekday:me,available:!1,remaining:null,peak:p?{id:p.id,name:p.name,message:p.message}:null,surcharge:(w=p==null?void 0:p.surcharge)!=null?w:0,waitlist:!1,reasons:[],slots:[]};g.dates.push(k);let ge=a.blockedWeekdays.get(me);ge&&k.reasons.push(ge);let fe=a.blockedDates.get(R);fe&&k.reasons.push(fe);for(let b of a.windows)(b.from&&R<b.from||b.to&&R>b.to)&&k.reasons.push(b.reason);let he=G(a.freshness,R,t.id);if(he&&k.reasons.push(he),k.reasons.length)continue;let{hours:pe,exception:ye,special:Pe}=te(t,n,R);if(ye){k.reasons.push(ye);continue}if(!pe.length){k.reasons.push({code:"CLOSED_WEEKLY",message:"Closed on this day"});continue}if(x===0&&s.sameDayCutoff&&y.minutes>=D(s.sameDayCutoff)){k.reasons.push({code:"PAST_CUTOFF",message:`Same-day orders close at ${s.sameDayCutoff}`});continue}if(x===1&&s.nextDayCutoff&&y.minutes>=D(s.nextDayCutoff)){k.reasons.push({code:"PAST_CUTOFF",message:`Next-day orders close at ${s.nextDayCutoff}`});continue}if(p!=null&&p.cutoff){let b=v(I(R,-p.cutoff.daysBefore),p.cutoff.time,i);if(o>=b){k.reasons.push({code:"PEAK_CUTOFF",message:`${p.name} orders closed on ${I(R,-p.cutoff.daysBefore)} at ${p.cutoff.time}`});continue}}let be=c(d+((P=p==null?void 0:p.extraPrepMinutes)!=null?P:0)),_e=z(t.id,n,R),ke=(N=r[_e])!=null?N:{day:0,slots:{}},Re=(ie=(_=p==null?void 0:p.dailyCapacity)!=null?_:s.dailyCapacity)!=null?ie:null,A=Re==null?null:Math.max(0,Re-ke.day);k.remaining=A;let Ce=qe(s,R,pe,!!Pe);if(!Ce.length){k.reasons.push({code:"CLOSED_WEEKLY",message:"No time slots on this day"});continue}for(let b of Ce){let U=v(R,b.start,i),T=v(R,b.end,i),Se=we(b.start,b.end),S={id:Se,start:b.start,end:b.end,startUtc:B(U),endUtc:B(T),available:!1,remaining:null,surcharge:(ae=b.surcharge)!=null?ae:0,waitlist:!1,reasons:[]};if(k.slots.push(S),g.dateOnly?T<=o?S.reasons.push({code:"SLOT_IN_PAST",message:"Closed for today"}):be>T&&S.reasons.push({code:"PREP_TIME",message:"Not enough preparation time"}):U<=o?S.reasons.push({code:"SLOT_IN_PAST",message:"This time has passed"}):be>U&&S.reasons.push({code:"PREP_TIME",message:"Not enough preparation time"}),S.reasons.length)continue;let xe=(ue=(de=(le=p==null?void 0:p.slotCapacity)!=null?le:b.capacity)!=null?de:s.slotCapacity)!=null?ue:null,Ae=(ce=ke.slots[Se])!=null?ce:0,W=xe==null?null:Math.max(0,xe-Ae);if(S.remaining=W==null?A:A==null?W:Math.min(W,A),A===0?S.reasons.push({code:"DAY_FULL",message:"Fully booked for this day"}):W===0&&S.reasons.push({code:"SLOT_FULL",message:"This time slot is full"}),S.reasons.length){S.waitlist=!!(p!=null&&p.waitlist);continue}S.available=!0}if(k.available=k.slots.some(b=>b.available),k.waitlist=!k.available&&k.slots.some(b=>b.waitlist),!k.available){let b=new Set;for(let U of k.slots)for(let T of U.reasons)b.has(T.code)||(b.add(T.code),k.reasons.push(T))}}return g}function ne(e){for(let t of e.dates){let n=t.slots.find(o=>o.available);if(n)return{date:t.date,slot:n}}return null}function Ze(e,t,n){let o=V(e);if(o.blockers.length)return{ok:!1,reasons:o.blockers};let r=o.dates.find(s=>s.date===t);if(!r)return{ok:!1,reasons:[{code:"BEYOND_HORIZON",message:"This date can't be booked"}]};let i=r.slots.find(s=>s.id===n);return i?i.available?{ok:!0,slot:i,date:r}:{ok:!1,reasons:i.reasons.length?i.reasons:r.reasons}:{ok:!1,reasons:r.reasons.length?r.reasons:[{code:"CLOSED_WEEKLY",message:"Slot not offered"}]}}function ze(e,t,n,o,r){var u,d,l,c,g,y;let i=e.methods[t],s=j(r,e.id,t,n),a;if((i==null?void 0:i.slotMode)==="explicit"){let[m,f]=o.split("-");for(let h of Object.values((u=i.slots)!=null?u:{}))for(let C of h!=null?h:[])C.start===m&&C.end===f&&C.capacity!=null&&(a=C.capacity)}return{dayCap:(l=(d=s==null?void 0:s.dailyCapacity)!=null?d:i==null?void 0:i.dailyCapacity)!=null?l:null,slotCap:(y=(g=(c=s==null?void 0:s.slotCapacity)!=null?c:a)!=null?g:i==null?void 0:i.slotCapacity)!=null?y:null}}var se=class{constructor(){O(this,"holds",new Map);O(this,"bookings",new Map);O(this,"waitlist",[]);O(this,"audit",[]);O(this,"seq",0)}counts(t,n,o,r){let i=0,s=0,a=u=>u.locationId===t.locationId&&u.method===t.method&&u.date===t.date;for(let u of this.bookings.values()){if(u.id===r||u.status!=="confirmed")continue;let d={locationId:u.locationId,method:u.method,date:u.date,slot:`${u.slotStart}-${u.slotEnd}`};a(d)&&(i++,d.slot===t.slot&&s++)}for(let u of this.holds.values())u.token===o||u.expiresAt<=n||a(u)&&(i++,u.slot===t.slot&&s++);return{day:i,slot:s}}usage(t){let n={},o=r=>{var s,a;let i=z(r.locationId,r.method,r.date);(s=n[i])!=null||(n[i]={day:0,slots:{}}),n[i].day++,n[i].slots[r.slot]=((a=n[i].slots[r.slot])!=null?a:0)+1};for(let r of this.bookings.values())r.status==="confirmed"&&o({locationId:r.locationId,method:r.method,date:r.date,slot:`${r.slotStart}-${r.slotEnd}`});for(let r of this.holds.values())r.expiresAt>t&&o(r);return n}fits(t,n,o,r,i){let s=this.counts(t,o,r,i);return n.dayCap!=null&&s.day>=n.dayCap?"DAY_FULL":n.slotCap!=null&&s.slot>=n.slotCap?"SLOT_FULL":null}hold(t,n,o,r,i){let s=this.fits(n,o,r,t);if(s)return{ok:!1,code:s,message:s==="DAY_FULL"?"Fully booked for this day":"This time slot was just taken"};let a={...n,token:t,expiresAt:r+i*6e4};return this.holds.set(t,a),{ok:!0,hold:a}}release(t){this.holds.delete(t)}sweep(t){let n=0;for(let[o,r]of this.holds)r.expiresAt<=t&&(this.holds.delete(o),n++);return n}confirm(t,n,o,r,i,s){let a=[...this.bookings.values()].find(m=>m.orderId===t);if(a)return{ok:!0,booking:a,duplicate:!0};let u=n?this.holds.get(n):void 0,d=u!=null?u:o,l=`bk_${++this.seq}`,c=m=>({...s,id:l,orderId:t,status:m,method:d.method,locationId:d.locationId,date:d.date,slotStart:d.slot.split("-")[0],slotEnd:d.slot.split("-")[1],rescheduleCount:0});if(!d){let m={...s,id:l,orderId:t,status:"needs_review",method:"pickup",locationId:"",date:""};return this.bookings.set(l,m),{ok:!1,booking:m,code:"NEEDS_REVIEW",message:"Order has no scheduling data"}}let g=u&&u.expiresAt>i;if(!g){let m=this.fits(d,r,i,n!=null?n:void 0);if(m){let f=c("needs_review");return this.bookings.set(l,f),n&&this.holds.delete(n),this.log(i,"system",l,"flagged",void 0,{reason:m}),{ok:!1,booking:f,code:"NEEDS_REVIEW",message:"Hold expired and the slot filled up"}}}let y=c("confirmed");return this.bookings.set(l,y),n&&this.holds.delete(n),this.log(i,"system",l,"confirmed"),{ok:!0,booking:y,duplicate:!1,reReserved:!g}}reschedule(t,n,o,r,i){var g;let s=this.bookings.get(t);if(!s||s.status!=="confirmed")return{ok:!1,message:"Booking can't be rescheduled"};let a=this.fits(n,o,r,void 0,t);if(a)return{ok:!1,message:a==="DAY_FULL"?"That day is fully booked":"That slot is full"};let u={...s},d={locationId:s.locationId,method:s.method,date:s.date,slot:`${s.slotStart}-${s.slotEnd}`},[l,c]=n.slot.split("-");return Object.assign(s,{locationId:n.locationId,method:n.method,date:n.date,slotStart:l,slotEnd:c,rescheduleCount:((g=s.rescheduleCount)!=null?g:0)+1}),this.log(r,i,t,"rescheduled",Q(u),Q(s)),{ok:!0,booking:s,freed:d}}cancel(t,n,o=!0){let r=[...this.bookings.values()].find(s=>s.orderId===t);if(!r||r.status==="cancelled")return r!=null?r:null;let i={...r};return r.status=o?"cancelled":r.status,this.log(n,"system",r.id,"cancelled",Q(i),Q(r)),r}joinWaitlist(t,n,o){let r={...t,id:`wl_${++this.seq}`,contact:n,createdAt:o};return this.waitlist.push(r),r}nextWaitlisted(t){var n;return(n=this.waitlist.find(o=>!o.notifiedAt&&o.locationId===t.locationId&&o.method===t.method&&o.date===t.date&&(o.slot===t.slot||o.slot==="*")))!=null?n:null}log(t,n,o,r,i,s){this.audit.push({at:t,actor:n,bookingId:o,action:r,before:i,after:s})}};function Q(e){return{locationId:e.locationId,method:e.method,date:e.date,slotStart:e.slotStart,slotEnd:e.slotEnd,status:e.status}}function oe(e,t,n){if(!e||!e[t])return!0;let o=e[t];return n.items.every(r=>{var i;return((i=o[r.variantId])!=null?i:0)>=r.quantity})}function je(e){var s,a,u;let t=F(e.cart,e.rules,e.settings.mixedPrepPolicy),n=(s=e.cart.subtotal)!=null?s:e.cart.items.reduce((d,l)=>d+l.price*l.quantity,0),o=[];for(let d of e.locations){let l={location:d,eligible:!0,reasons:[],distanceKm:null,first:null};if(o.push(l),e.method==="delivery"){let y=J(d,(a=e.address)!=null?a:{});l.zone=y,l.distanceKm=y.distanceKm,y.zone?(l.fee=ee(y.zone,n),l.fee.belowMinimum&&(l.eligible=!1,l.reasons.push({code:"BELOW_MINIMUM_ORDER",message:`Delivery needs a minimum order of $${(l.fee.minOrder/100).toFixed(2)}`}))):(l.eligible=!1,l.reasons.push(y.reason))}e.method!=="shipping"&&!oe(e.inventory,d.id,e.cart)&&(l.eligible=!1,l.reasons.push({code:"OUT_OF_STOCK_AT_LOCATION",message:`Not all items are in stock at ${d.name}`}));let c=V({...e,location:d,constraints:t});l.availability=c,c.blockers.length&&(l.eligible=!1,l.reasons.push(...c.blockers));let g=ne(c);l.first=g?{date:g.date,slot:g.slot.id}:null,l.eligible&&!g&&(l.eligible=!1,l.reasons.push({code:"DAY_FULL",message:"No available dates in the booking window"}))}let r=(u=e.ranking)!=null?u:"priority";o.sort((d,l)=>{if(d.eligible!==l.eligible)return d.eligible?-1:1;if(r==="distance"&&d.distanceKm!=null&&l.distanceKm!=null&&d.distanceKm!==l.distanceKm)return d.distanceKm-l.distanceKm;if(r==="earliest"&&d.first&&l.first){let c=`${d.first.date} ${d.first.slot}`,g=`${l.first.date} ${l.first.slot}`;if(c!==g)return c<g?-1:1}return d.location.priority-l.location.priority});let i=null;if(e.inventory&&e.method!=="shipping"&&!o.some(d=>d.eligible)){let d=e.cart.items.every(c=>e.locations.reduce((g,y)=>{var m,f;return g+((f=(m=e.inventory[y.id])==null?void 0:m[c.variantId])!=null?f:0)},0)>=c.quantity),l=e.locations.some(c=>oe(e.inventory,c.id,e.cart));d&&!l&&(i={code:"OUT_OF_STOCK_AT_LOCATION",message:"Your items are stocked at different locations. Choose shipping, or remove some items to schedule a single pickup or delivery."})}return{ranked:o,splitRequired:i}}function re(e){var t;return v(e.date,(t=e.slotStart)!=null?t:"00:00",e.timezone)}function Ve(e,t,n){var r;if(!t.enabled)return{ok:!1,reason:"Rescheduling isn't available for this store. Please contact us."};if(e.status!=="confirmed")return{ok:!1,reason:"This booking can't be changed."};if(((r=e.rescheduleCount)!=null?r:0)>=t.maxChanges)return{ok:!1,reason:`You've already changed this booking ${t.maxChanges} time(s). Please contact us.`};let o=re(e)-t.minHoursBefore*36e5;return n>=o?{ok:!1,reason:`Changes are allowed up to ${t.minHoursBefore} hours before your time slot.`}:{ok:!0,deadline:o}}function Qe(e,t,n,o={}){let r=re(e);return t.filter(i=>{var s;return i.enabled&&(!((s=i.methods)!=null&&s.length)||i.methods.includes(e.method))}).map(i=>{var s,a,u;return{policyId:i.id,channel:i.channel,sendAt:r-i.minutesBefore*6e4,body:Le(i.template,{name:(a=(s=e.customer)==null?void 0:s.name)!=null?a:"there",order:(u=e.orderName)!=null?u:"",date:e.date,time:e.slotStart&&e.slotEnd?`${e.slotStart}\u2013${e.slotEnd}`:"",method:e.method,...o})}}).filter(i=>i.sendAt>n).filter(i=>{var s,a;return i.channel==="email"?!!((s=e.customer)!=null&&s.email):!!((a=e.customer)!=null&&a.phone)})}function Le(e,t){return e.replace(/\{\{\s*(\w+)\s*\}\}/g,(n,o)=>{var r;return(r=t[o])!=null?r:""})}function Xe(e,t,n){var c,g,y;let o=e.filter(m=>m.method==="delivery"&&m.locationId===t.id&&m.date===n&&m.status==="confirmed"),r=o.filter(m=>{var f,h;return((f=m.address)==null?void 0:f.lat)!=null&&((h=m.address)==null?void 0:h.lng)!=null}),i=o.filter(m=>!r.includes(m)),s=new Map;for(let m of r){let f=`${m.slotStart}-${m.slotEnd}`;s.set(f,[...(c=s.get(f))!=null?c:[],m])}let a=[],u={lat:(g=t.lat)!=null?g:0,lng:(y=t.lng)!=null?y:0},d=0,l=t.lat!=null&&t.lng!=null;for(let m of[...s.keys()].sort()){let f=[...s.get(m)];for(;f.length;){let h=0,C=1/0;f.forEach((P,N)=>{let _=Z(u.lat,u.lng,P.address.lat,P.address.lng);_<C&&(C=_,h=N)});let[M]=f.splice(h,1),w=l||a.length?C:null;w!=null&&(d+=w),a.push({seq:a.length+1,booking:M,window:m,legKm:w}),u={lat:M.address.lat,lng:M.address.lng}}}return{locationId:t.id,date:n,stops:a,totalKm:a.length?Math.round(d*10)/10:null,unlocated:i,mapsUrls:Ee(t,a)}}function Ee(e,t){if(!t.length)return[];let n=s=>`${s.address.lat},${s.address.lng}`,o=e.lat!=null?`${e.lat},${e.lng}`:encodeURIComponent(e.address),r=[],i=o;for(let s=0;s<t.length;s+=10){let a=t.slice(s,s+10),u=n(a[a.length-1].booking),d=a.slice(0,-1).map(l=>n(l.booking)).join("|");r.push(`https://www.google.com/maps/dir/?api=1&origin=${i}&destination=${u}`+(d?`&waypoints=${encodeURIComponent(d)}`:"")+"&travelmode=driving"),i=u}return r}function Ge(e,t){var r;let n=t!=null?t:Object.keys((r=e[0])!=null?r:{}),o=i=>{let s=i==null?"":String(i);return/[",\r\n]/.test(s)?`"${s.replace(/"/g,'""')}"`:s};return"\uFEFF"+[n.join(","),...e.map(i=>n.map(s=>o(i[s])).join(","))].join(`\r
`)}return Ke(Je);})();
</script>
<script>
(function(){
"use strict";
var E = window.DatewiseEngine;
var TZ = "America/Toronto";
/* ---------- formatting ---------- */
function money(c){ return "$" + (c/100).toFixed(2); }
function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c];}); }
function t12(hhmm){ var p=hhmm.split(":"),h=+p[0],m=p[1]; var ap=h>=12?"PM":"AM"; h=h%12||12; return h+(m==="00"?"":":"+m)+" "+ap; }
function slotLabel(s,e){ return t12(s)+" – "+t12(e); }
function dLabel(d,opts){ return new Date(d+"T12:00:00Z").toLocaleDateString("en-CA",Object.assign({weekday:"short",month:"short",day:"numeric",timeZone:"UTC"},opts||{})); }
function nowLabel(ms){ var p=E.localParts(ms,TZ); return dLabel(p.date)+" · "+t12(p.time); }
var METHOD_NAME={pickup:"Store pickup",delivery:"Local delivery",shipping:"Shipping"};
/* ---------- simulated clock ---------- */
var clock={base:Date.now(),setAt:Date.now()};
function now(){ return clock.base + (Date.now()-clock.setAt); }
function setClock(ms){ clock.base=ms; clock.setAt=Date.now(); }
/* ---------- sample configuration ---------- */
function hrs(a,b){ return [{start:a,end:b}]; }
function week(days,a,b){ var o={}; days.forEach(function(d){o[d]=hrs(a,b);}); return o; }
var dSlots=[{start:"10:00",end:"13:00"},{start:"13:00",end:"16:00"},{start:"16:00",end:"19:00",surcharge:300}];
function explicit(days){ var o={}; days.forEach(function(d){o[d]=dSlots;}); return o; }
var LOCS=[{
id:"kit",name:"Kitchener — Guelph St",address:"Unit 7, 900 Guelph St, Kitchener, ON N2H 5Z6",timezone:TZ,lat:43.4643,lng:-80.4844,enabled:true,priority:1,
pickupInstructions:"Unit 7. Ring the bell at the side door.",
exceptions:[{date:"2026-11-08",hours:hrs("09:00","15:00"),methods:["pickup"],note:"Diwali Sunday pickup hours"},{date:"2026-10-26",closed:true,methods:["pickup"],note:"Closed for stock count"}],
zones:[{id:"kw",name:"Kitchener–Waterloo",postalPatterns:["N2A*","N2B*","N2C*","N2E*","N2G*","N2H*","N2K*","N2M*","N2P*","N2R*"],fee:799,freeOver:7500,minOrder:2500},
{id:"r25",name:"Within 25 km",radiusKm:25,fee:1299,minOrder:4000}],
methods:{
pickup:{enabled:true,weeklyHours:week([1,2,3,4,5,6],"10:00","18:00"),slotMode:"generated",slotMinutes:60,prepMinutes:60,prepMode:"elapsed",sameDayCutoff:"14:00",horizonDays:30,slotCapacity:4},
delivery:{enabled:true,weeklyHours:week([0,2,3,4,5,6],"10:00","19:00"),slotMode:"explicit",slots:explicit([0,2,3,4,5,6]),prepMinutes:120,prepMode:"elapsed",sameDayCutoff:"12:00",horizonDays:30,slotCapacity:5,dailyCapacity:12},
shipping:{enabled:true,weeklyHours:week([1,2,3,4,5],"10:00","17:00"),slotMode:"dateOnly",prepMinutes:0,prepMode:"operating",sameDayCutoff:"14:00",horizonDays:14}
}
},{
id:"wat",name:"Waterloo — King St (sample)",address:"King St N, Waterloo, ON",timezone:TZ,lat:43.4723,lng:-80.5449,enabled:true,priority:2,
pickupInstructions:"Front counter.",exceptions:[],
zones:[{id:"wl",name:"Waterloo",postalPatterns:["N2J*","N2L*","N2T*","N2V*"],fee:599,freeOver:6000,minOrder:2000}],
methods:{
pickup:{enabled:true,weeklyHours:week([2,3,4,5,6],"11:00","19:00"),slotMode:"generated",slotMinutes:120,prepMinutes:90,prepMode:"elapsed",sameDayCutoff:"15:00",horizonDays:30,slotCapacity:3},
delivery:{enabled:true,weeklyHours:week([3,5,6],"12:00","18:00"),slotMode:"generated",slotMinutes:180,prepMinutes:180,prepMode:"elapsed",sameDayCutoff:"11:00",horizonDays:21,slotCapacity:4,dailyCapacity:8}
}
}];
var PRODUCTS=[
{productId:"p_marigold",variantId:"v_marigold",title:"Fresh Marigold Garland",price:1999,collectionIds:["c_flowers"],vendor:"C2C Agricorp",productType:"Fresh Flowers",tags:["perishable"],color:"#D98A04",abbr:"MG"},
{productId:"p_pakku",variantId:"v_pakku",title:"பாக்கு (Pākkŭ) 200 g",price:699,collectionIds:["c_puja"],vendor:"Clicks to Cart",productType:"Puja",tags:[],color:"#7A4E2D",abbr:"PK"},
{productId:"p_diya",variantId:"v_diya",title:"Brass Diya Set (4)",price:2499,collectionIds:["c_puja","c_diwali"],vendor:"Clicks to Cart",productType:"Puja",tags:[],color:"#9C7A1C",abbr:"DY"},
{productId:"p_saree",variantId:"v_saree",title:"Kanchipuram Silk Saree",price:15999,collectionIds:["c_apparel"],vendor:"Clicks to Cart",productType:"Apparel",tags:[],color:"#8E1F4A",abbr:"SR"}
];
var INVENTORY={kit:{v_marigold:40,v_pakku:80,v_diya:30,v_saree:6},wat:{v_marigold:20,v_pakku:40,v_diya:12,v_saree:0}};
var RULES=[
{id:"r_noship",name:"Fresh flowers can't ship",enabled:true,priority:1,target:{type:"productType",values:["Fresh Flowers"]},action:{type:"prohibitMethods",methods:["shipping"]},message:"Fresh flowers are pickup or local delivery only"},
{id:"r_prep_fresh",name:"Garland stringing: +4 h",enabled:true,priority:2,target:{type:"tag",values:["perishable"]},action:{type:"addPrep",minutes:240}},
{id:"r_prep_saree",name:"Saree fall & pico: +1 day",enabled:true,priority:3,target:{type:"collection",values:["c_apparel"]},action:{type:"addPrep",minutes:1440}},
{id:"r_fresh",name:"Marigold air shipments",enabled:true,priority:4,target:{type:"product",values:["p_marigold"]},
action:{type:"freshness",arrivals:[{date:"2026-10-13"},{date:"2026-10-20"},{date:"2026-10-27"},{date:"2026-11-03"},{date:"2026-11-05"}],minDaysAfterArrival:1,shelfLifeDays:4}}
];
var PEAKS=[{id:"pk_diwali",name:"Diwali",dates:["2026-11-07","2026-11-08"],methods:["delivery","pickup"],dailyCapacity:30,slotCapacity:10,extraPrepMinutes:60,cutoff:{daysBefore:2,time:"18:00"},surcharge:500,waitlist:true,message:"Diwali rush: orders for Nov 7–8 close two days earlier at 6 PM"}];
var SETTINGS={mixedPrepPolicy:"longest",holdMinutes:10,weekStartsOn:0,
reschedule:{enabled:true,minHoursBefore:24,maxChanges:2,allowMethodChange:false},
reminders:[
{id:"rm1",channel:"whatsapp",minutesBefore:1440,enabled:true,template:"Hi {{name}}, your {{method}} for {{order}} is tomorrow, {{date}} {{time}}. Need to change it? Use the link in your order page."},
{id:"rm2",channel:"sms",minutesBefore:120,enabled:true,methods:["delivery"],template:"{{name}}, your driver is out for delivery {{time}} today. Order {{order}}."},
{id:"rm3",channel:"email",minutesBefore:1440,enabled:true,methods:["pickup"],template:"Your pickup for {{order}} is ready {{date}} {{time}}."}]};
var GEO={N2H:[43.455,-80.49],N2G:[43.445,-80.49],N2M:[43.44,-80.51],N2E:[43.42,-80.47],N2A:[43.45,-80.43],N2B:[43.47,-80.46],N2K:[43.48,-80.47],N2P:[43.40,-80.43],N2R:[43.40,-80.48],N2C:[43.43,-80.45],N2J:[43.47,-80.52],N2L:[43.475,-80.535],N2T:[43.455,-80.565],N2V:[43.50,-80.55],N3C:[43.39,-80.32],N3H:[43.40,-80.33],N1H:[43.545,-80.25],N3A:[43.36,-80.44],M5V:[43.64,-79.39],L5B:[43.59,-79.64]};
/* ---------- reservation store + seeded bookings ---------- */
var store=new E.MemoryReservationStore();
var NAMES=["Anjali R.","Karthik S.","Meena P.","Ravi K.","Deepa N.","Suresh V.","Lakshmi T.","Arjun M.","Priya D.","Vijay A.","Kavya B.","Hari G.","Nisha J.","Rahul C.","Divya L.","Ganesh O."];
var STREETS=["King St E","Weber St","Victoria St N","Highland Rd W","Fischer-Hallman Rd","Ottawa St S","Belmont Ave","Queen St S","Frederick St","Lancaster St W"];
var seed=7; function rnd(){ seed=(seed*16807)%2147483647; return (seed-1)/2147483646; }
var orderNo=1000;
function addBooking(b){ var id="bk_s"+(++orderNo); b.id=id; b.orderId="o"+orderNo; b.orderName="#"+orderNo; b.timezone=TZ; b.rescheduleCount=0; store.bookings.set(id,b); return b; }
(function seedBookings(){
var start="2026-10-10";
for(var i=0;i<56;i++){
var date=E.addDays(start,i), wd=E.weekdayOf(date);
var peak=PEAKS[0].dates.indexOf(date)>=0;
if([0,2,3,4,5,6].indexOf(wd)>=0 || peak){
var n=peak?(date==="2026-11-08"?27:22):Math.floor(3+rnd()*7);
for(var k=0;k<n;k++){
var s=peak&&date==="2026-11-08"&&k<10?dSlots[0]:dSlots[Math.floor(rnd()*3)];
var lat=43.4643+(rnd()-.5)*0.09, lng=-80.4844+(rnd()-.5)*0.13;
addBooking({status:"confirmed",method:"delivery",locationId:"kit",date:date,slotStart:s.start,slotEnd:s.end,
customer:{name:NAMES[Math.floor(rnd()*NAMES.length)],email:"customer"+orderNo+"@example.com",phone:"+1519555"+(1000+orderNo)},
address:{line1:(10+Math.floor(rnd()*400))+" "+STREETS[Math.floor(rnd()*STREETS.length)],city:"Kitchener",postal:"N2H",lat:lat,lng:lng},
items:[{title:"Fresh Marigold Garland",quantity:1+Math.floor(rnd()*3)}],peakDayId:peak?"pk_diwali":null});
}
}
if(wd>=1&&wd<=6){
var p=Math.floor(rnd()*5);
for(var q=0;q<p;q++){
var h=10+Math.floor(rnd()*8); var hh=(h<10?"0":"")+h+":00", he=(h+1<10?"0":"")+(h+1)+":00";
addBooking({status:"confirmed",method:"pickup",locationId:"kit",date:date,slotStart:hh,slotEnd:he,customer:{name:NAMES[Math.floor(rnd()*NAMES.length)],email:"pickup"+orderNo+"@example.com"},items:[{title:"பாக்கு (Pākkŭ) 200 g",quantity:1}]});
}
}
}
addBooking({status:"needs_review",method:"delivery",locationId:"kit",date:"2026-11-08",slotStart:"10:00",slotEnd:"13:00",customer:{name:"Sangeetha R.",email:"sangeetha@example.com",phone:"+15195550177"},address:{line1:"55 Weber St",postal:"N2H"},items:[{title:"Fresh Marigold Garland",quantity:4}],note:"Hold expired while paying and the slot filled up"});
addBooking({status:"needs_review",method:"pickup",locationId:"",date:"",customer:{name:"Mohan K.",email:"mohan@example.com"},items:[{title:"Brass Diya Set (4)",quantity:1}],note:"Order came through Buy now with no date selected"});
store.joinWaitlist({locationId:"kit",method:"delivery",date:"2026-11-08",slot:"10:00-13:00"},{name:"Uma S.",email:"uma@example.com"},Date.now());
store.joinWaitlist({locationId:"kit",method:"delivery",date:"2026-11-08",slot:"10:00-13:00"},{name:"Bala V.",phone:"+15195550190"},Date.now());
})();
/* ---------- state ---------- */
var S={view:"shop",tab:"overview",cart:{v_marigold:2,v_pakku:1,v_diya:1,v_saree:0},method:"delivery",locId:null,postal:"N2H 5Z6",
month:null,date:null,sel:null,token:null,notice:null,lastOrder:null,resched:null,
bf:{status:"all",method:"all",from:"",loc:"all"},run:{date:null},diag:{method:"delivery",loc:"kit",date:null},
customer:{name:"Priya Raman",email:"priya@example.com",phone:"+1 519 555 0142"},waitEmail:""};
function cart(){ var items=[]; PRODUCTS.forEach(function(p){ var q=S.cart[p.variantId]||0; if(q>0) items.push(Object.assign({},p,{quantity:q})); }); return {items:items,subtotal:items.reduce(function(s,i){return s+i.price*i.quantity;},0)}; }
function geocode(postal){ var n=E.normalizePostal(postal); var g=GEO[n.slice(0,3)]; return g?{postal:postal,lat:g[0],lng:g[1]}:{postal:postal}; }
function locById(id){ return LOCS.filter(function(l){return l.id===id;})[0]; }
function usageExcludingMine(){ var mine=S.token&&store.holds.get(S.token); if(mine) store.holds.delete(S.token); var u=store.usage(now()); if(mine) store.holds.set(S.token,mine); return u; }
function ctx(extra){ return Object.assign({locations:LOCS,method:S.method,cart:cart(),rules:RULES,peakDays:PEAKS,usage:usageExcludingMine(),settings:SETTINGS,now:now(),address:geocode(S.postal),inventory:INVENTORY,ranking:"priority"},extra||{}); }
function myHold(){ var h=S.token&&store.holds.get(S.token); return h&&h.expiresAt>now()?h:null; }
/* ---------- storefront ---------- */
function computeShop(){
var c=cart(); if(!c.items.length) return {empty:true};
var res=E.rankLocations(ctx());
var chosen=null;
res.ranked.forEach(function(r){ if(r.location.id===S.locId && r.eligible) chosen=r; });
if(!chosen) chosen=res.ranked.filter(function(r){return r.eligible;})[0]||null;
return {ranked:res.ranked,split:res.splitRequired,chosen:chosen,cart:c};
}
function methodState(m){
var c=cart(); if(!c.items.length) return {ok:false};
var r=E.rankLocations(ctx({method:m}));
var ok=r.ranked.some(function(x){return x.eligible;});
var why=ok?"":((r.splitRequired&&r.splitRequired.message)||(r.ranked[0]&&r.ranked[0].reasons[0]&&r.ranked[0].reasons[0].message)||"Unavailable");
var first=null; r.ranked.forEach(function(x){ if(x.eligible&&x.first&&(!first||x.first.date<first.date)) first=x.first; });
return {ok:ok,why:why,first:first};
}
function revalidate(sh){
if(!S.sel) return;
var reason=null;
if(!sh.chosen || sh.chosen.location.id!==S.sel.locationId || S.method!==S.sel.method) reason="Your delivery choice changed, so the time you picked was released.";
else {
var d=sh.chosen.availability.dates.filter(function(x){return x.date===S.sel.date;})[0];
var s=d&&d.slots.filter(function(x){return x.id===S.sel.slot;})[0];
if(!s||!s.available){ var rs=(s&&s.reasons[0])||(d&&d.reasons[0]); reason="Your selected time is no longer available"+(rs?": "+rs.message.charAt(0).toLowerCase()+rs.message.slice(1):"")+". Please choose another."; }
}
if(reason){ if(S.token) store.release(S.token); S.sel=null; S.notice={kind:"bad",text:reason}; }
}
function renderShop(){
if(S.lastOrder && !S.resched) return renderConfirmation();
var sh=computeShop(); var c=cart();
if(!sh.empty) revalidate(sh);
var h='';
if(S.notice) h+='<div class="notice '+S.notice.kind+'" role="status">'+esc(S.notice.text)+'</div>';
h+='<div class="store">';
// cart
h+='<section class="panel" aria-labelledby="cartH"><div class="storehead"><h2 id="cartH">Your cart</h2><span class="muted small">Change items to see dates update</span></div>';
PRODUCTS.forEach(function(p){
var q=S.cart[p.variantId]||0; var tags='';
if(p.tags.indexOf("perishable")>=0) tags+='<span class="tag fresh">Fresh · ships by air from India</span>';
if(p.productType==="Apparel") tags+='<span class="tag">Tailoring +1 day</span>';
if(p.variantId==="v_saree") tags+='<span class="tag">Not stocked at Waterloo</span>';
h+='<div class="item"><div class="thumb" style="background:'+p.color+'">'+p.abbr+'</div><div><div class="t">'+esc(p.title)+'</div><div class="tags"><span class="tag">'+money(p.price)+'</span>'+tags+'</div></div>'+
'<div class="qty"><button aria-label="Remove one '+esc(p.title)+'" data-act="qty" data-v="'+p.variantId+'" data-d="-1">−</button><span>'+q+'</span><button aria-label="Add one '+esc(p.title)+'" data-act="qty" data-v="'+p.variantId+'" data-d="1">+</button></div></div>';
});
var fee=feeQuote(sh);
h+='<div class="totals"><span>Subtotal</span><span class="num">'+money(c.subtotal)+'</span>';
if(fee){ h+='<span>'+esc(METHOD_NAME[S.method])+' fee'+(fee.waived?' (free over '+money(sh.chosen.zone.zone.freeOver)+')':'')+'</span><span class="num">'+money(fee.waived?0:fee.baseFee)+'</span>';
fee.surcharges.forEach(function(x){ h+='<span>'+esc(x.label)+'</span><span class="num">'+money(x.amount)+'</span>'; }); }
h+='<span class="grand">Total</span><span class="grand num">'+money(c.subtotal+(fee?fee.fee:0))+'</span></div>';
if(fee) h+='<p class="muted small">Fees are added to the cart as a “Delivery &amp; handling” line, so they are charged at checkout on any Shopify plan.</p>';
h+='</section>';
// widget
h+='<section class="panel widget" aria-labelledby="wH"><h2 id="wH">When would you like it?</h2>';
if(sh.empty){ h+='<p class="muted">Add an item to choose a date.</p></section></div>'; return h; }
h+='<div class="methods" role="group" aria-label="Delivery method">';
["delivery","pickup","shipping"].forEach(function(m){
var st=methodState(m);
h+='<button class="mbtn" data-act="method" data-v="'+m+'" aria-pressed="'+(S.method===m)+'"'+(st.ok||S.method===m?'':' disabled')+'><b>'+METHOD_NAME[m]+'</b><small>'+(st.ok?(st.first?'From '+dLabel(st.first.date):''):esc(st.why))+'</small></button>';
});
h+='</div>';
if(S.method==="delivery"){
h+='<div class="row"><div class="field" style="flex:1"><label class="label" for="postal">Delivery postal code</label><input type="text" id="postal" value="'+esc(S.postal)+'" autocomplete="postal-code" placeholder="e.g. N2H 5Z6"></div><button class="btn" data-act="postal" style="align-self:end">Check</button></div>';
h+='<p class="muted small" style="margin:6px 0 0">Try N2L 3G1 (Waterloo), N3C 2V4 (Cambridge, radius zone), or M5V 2T6 (Toronto, outside area).</p>';
}
if(sh.split) h+='<div class="notice bad" style="margin-top:12px">'+esc(sh.split.message)+'</div>';
if(S.method!=="shipping"){
h+='<div class="label" style="margin-top:14px">'+(S.method==="pickup"?"Pickup location":"Delivering from")+'</div><div class="loclist">';
sh.ranked.forEach(function(r){
var sel=sh.chosen&&sh.chosen.location.id===r.location.id;
var right=r.eligible?(r.fee?(r.fee.waived?'Free':money(r.fee.baseFee)):(r.distanceKm!=null?r.distanceKm.toFixed(1)+' km':'')):'';
h+='<button class="loc" data-act="loc" data-v="'+r.location.id+'" aria-pressed="'+sel+'"'+(r.eligible?'':' disabled')+'><span class="dot"></span><span><b>'+esc(r.location.name)+'</b><br><span class="small muted">'+(r.eligible?(r.first?'Next available '+dLabel(r.first.date):'')+(r.zone&&r.zone.zone?' · '+esc(r.zone.zone.name):''):esc(r.reasons[0]&&r.reasons[0].message))+'</span></span><span class="small num">'+right+'</span></button>';
});
h+='</div>';
}
if(!sh.chosen){ h+='<div class="notice bad" style="margin-top:12px">No location can take this order with '+esc(METHOD_NAME[S.method].toLowerCase())+'. Try another method or change your cart.</div></section></div>'; return h; }
var av=sh.chosen.availability;
var cons=E.evaluateRules(c,RULES,SETTINGS.mixedPrepPolicy);
if(cons.prepDriver) h+='<p class="small muted" style="margin:10px 0 0">Earliest times include '+fmtMins(av.prepMinutes)+' preparation, set by <b>'+esc(cons.prepDriver.itemTitle)+'</b>.</p>';
var peakDates=av.dates.filter(function(d){return d.peak&&d.peak.message;});
if(peakDates.length) h+='<div class="notice peak" style="margin:10px 0 0">'+esc(peakDates[0].peak.message)+(peakDates[0].surcharge?'. Peak-day handling +'+money(peakDates[0].surcharge)+'.':'')+'</div>';
h+=renderCalendar(av);
h+=renderSlots(av,sh);
h+='</section></div>';
return h;
}
function fmtMins(m){ if(m>=1440&&m%60===0) return Math.round(m/60)+' h'; if(m>=60) return (m/60).toFixed(m%60?1:0)+' h'; return m+' min'; }
function feeQuote(sh){
if(!sh||sh.empty||!sh.chosen) return null;
var extras=[];
if(S.sel){
var d=sh.chosen.availability.dates.filter(function(x){return x.date===S.sel.date;})[0];
var s=d&&d.slots.filter(function(x){return x.id===S.sel.slot;})[0];
if(s&&s.surcharge) extras.push({label:"Evening slot",amount:s.surcharge});
if(d&&d.peak&&d.surcharge) extras.push({label:d.peak.name+" peak handling",amount:d.surcharge});
}
if(S.method==="delivery") return E.quoteFee(sh.chosen.zone.zone,sh.cart.subtotal,extras);
if(extras.length) return E.quoteFee(null,sh.cart.subtotal,extras);
return null;
}
function renderCalendar(av){
var map={}; av.dates.forEach(function(d){map[d.date]=d;});
var today=E.localParts(now(),TZ).date;
if(!S.month) S.month=today.slice(0,7);
var first=S.month+"-01", wd=E.weekdayOf(first);
var h='<div class="cal"><div class="calhead"><button class="btn ghost" data-act="month" data-d="-1" aria-label="Previous month">‹</button><b>'+new Date(first+"T12:00:00Z").toLocaleDateString("en-CA",{month:"long",year:"numeric",timeZone:"UTC"})+'</b><button class="btn ghost" data-act="month" data-d="1" aria-label="Next month">›</button></div><div class="grid7">';
["S","M","T","W","T","F","S"].forEach(function(x,i){ h+='<div class="dow" aria-hidden="true">'+["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][i]+'</div>'; });
for(var i=0;i<wd;i++) h+='<div></div>';
for(var day=1;day<=31;day++){
var date=S.month+"-"+(day<10?"0":"")+day;
if(E.addDays(first,day-1)!==date) break;
var d=map[date]; var cls="day", lab=dLabel(date,{weekday:"long",month:"long"});
var extra='';
if(!d){ cls+=" out"; lab+=", not bookable"; }
else if(d.available){ cls+=" avail"; lab+=", available"; if(d.remaining!=null&&d.remaining<=5) extra='<span class="lft">'+d.remaining+' left</span>'; }
else if(d.waitlist||(d.reasons[0]&&/FULL/.test(d.reasons[0].code))){ cls+=" full"; lab+=", fully booked"; extra='<span class="lft">full</span>'; }
else { cls+=" closed"; lab+=", "+(d.reasons[0]?d.reasons[0].message:"unavailable"); }
if(S.date===date) cls+=" sel";
var pk=d&&d.peak?'<span class="pk" title="'+esc(d.peak.name)+'"></span>':'';
h+='<button class="'+cls+'" data-act="date" data-v="'+date+'" aria-label="'+esc(lab)+'" title="'+esc(d&&!d.available&&d.reasons[0]?d.reasons[0].message:"")+'"'+(d?'':' disabled')+'>'+day+pk+extra+'</button>';
}
h+='</div><div class="legend"><span><i style="background:var(--accent-soft)"></i>Available</span><span><i style="background:var(--warn-soft)"></i>Fully booked</span><span><i style="background:var(--peak);border-radius:50%"></i>Festival day</span></div></div>';
return h;
}
function renderSlots(av,sh){
if(!S.date) return '<p class="muted small" style="margin-top:12px">Pick a highlighted date.</p>';
var d=av.dates.filter(function(x){return x.date===S.date;})[0];
if(!d) return '';
var h='<div style="margin-top:14px"><div class="sectionhead"><h3>'+dLabel(d.date,{weekday:"long",month:"long"})+(d.peak?' <span class="pill peak">'+esc(d.peak.name)+'</span>':'')+'</h3>';
if(av.dateOnly) h+='<span class="small muted">Dispatch date, not an arrival promise</span>';
h+='</div>';
if(!d.available){
h+='<div class="notice '+(d.waitlist?'peak':'bad')+'" style="margin-top:8px">'+esc(d.reasons.map(function(r){return r.message;}).join(". "))+'</div>';
if(d.waitlist){
h+='<div class="row"><input type="email" id="waitEmail" placeholder="Email for a spot" value="'+esc(S.waitEmail)+'" style="flex:1"><button class="btn" data-act="waitlist">Join the waitlist</button></div><p class="small muted">We hold the next freed place for you for 30 minutes and message you first.</p>';
}
return h+'</div>';
}
var mine=myHold();
h+='<div class="slots" role="group" aria-label="Time slots">';
d.slots.forEach(function(s){
var selected=S.sel&&S.sel.date===d.date&&S.sel.slot===s.id;
var cap=s.remaining; var meter='';
if(cap!=null){ var capTot=E.capsFor(sh.chosen.location,S.method,d.date,s.id,PEAKS); var tot=Math.min(capTot.slotCap||cap,capTot.dayCap||cap)||cap; var pct=tot?Math.round(100*cap/Math.max(tot,cap)):0; meter='<div class="meter'+(cap<=2?' low':'')+'"><i style="width:'+pct+'%"></i></div>'; }
var sub=s.available?(cap!=null?cap+' left':'Open')+(s.surcharge?' · +'+money(s.surcharge):''):(s.reasons[0]?s.reasons[0].message:'');
if(selected&&mine) sub='Held for you';
h+='<button class="slot" data-act="slot" data-v="'+s.id+'" aria-pressed="'+selected+'"'+(s.available||selected?'':' disabled')+'><b>'+(av.dateOnly?'Ships this day':slotLabel(s.start,s.end))+'</b>'+meter+'<small>'+esc(sub)+'</small></button>';
});
h+='</div>';
if(S.sel){
var held=myHold();
h+='<div class="hold"><span>'+(held?'Your time is held for <b class="mono" id="holdLeft"></b>':'Your hold expired. Tap the time again to re-check it.')+'</span>'+(held?'<span class="small muted">Other shoppers see this place as taken.</span>':'')+'</div>';
var loc=sh.chosen.location;
h+='<dl class="summary"><dt>Method</dt><dd>'+METHOD_NAME[S.method]+'</dd><dt>'+(S.method==="pickup"?"Pick up at":"From")+'</dt><dd>'+esc(loc.name)+'</dd>';
h+='<dt>'+(av.dateOnly?'Dispatch':'Date')+'</dt><dd>'+dLabel(S.sel.date,{weekday:"long",month:"long"})+'</dd>';
if(!av.dateOnly) h+='<dt>Time</dt><dd>'+slotLabel(S.sel.slot.split("-")[0],S.sel.slot.split("-")[1])+'</dd>';
if(S.method==="pickup"&&loc.pickupInstructions) h+='<dt>Note</dt><dd>'+esc(loc.pickupInstructions)+'</dd>';
h+='</dl>';
h+='<div class="co"><input type="text" id="cName" aria-label="Name" value="'+esc(S.customer.name)+'"><input type="email" id="cEmail" aria-label="Email" value="'+esc(S.customer.email)+'"><input type="tel" id="cPhone" aria-label="Phone" value="'+esc(S.customer.phone)+'"></div>';
h+='<button class="btn primary" style="width:100%;margin-top:10px;padding:11px" data-act="checkout">Check out (demo order)</button>';
h+='<div class="demo"><span class="label">Try the edge cases</span><div class="row"><button class="btn sm" data-act="rush">Other shoppers take every remaining place in this slot</button><button class="btn sm" data-act="skip">Wait 11 minutes</button></div><span class="small muted">While your hold is live, the rush can’t take your place. After it expires, it can.</span></div>';
}
return h+'</div>';
}
function renderConfirmation(){
var b=store.bookings.get(S.lastOrder);
var loc=locById(b.locationId);
var rem=E.planReminders(b,SETTINGS.reminders,now(),{date:dLabel(b.date,{weekday:"long",month:"long"}),time:b.slotStart?slotLabel(b.slotStart,b.slotEnd):""});
var can=E.canCustomerReschedule(b,SETTINGS.reschedule,now());
var h='<div class="notice ok">Order '+esc(b.orderName)+' placed. This is what the customer sees on the thank-you and order status pages.</div>';
h+='<div class="store"><section class="panel confirm"><span class="label">Order status page</span><h2>'+METHOD_NAME[b.method]+', '+dLabel(b.date,{weekday:"long",month:"long"})+'</h2>';
h+='<dl class="summary"><dt>Time</dt><dd>'+(b.slotStart&&b.method!=="shipping"?slotLabel(b.slotStart,b.slotEnd):'Dispatch day')+'</dd><dt>Location</dt><dd>'+esc(loc.name)+'</dd><dt>Status</dt><dd><span class="pill '+b.status+'">'+b.status.replace("_"," ")+'</span>'+(b.rescheduleCount?' · changed '+b.rescheduleCount+'×':'')+'</dd></dl>';
if(can.ok){ h+='<div><button class="btn primary" data-act="resched-open">Change date or time</button> <span class="small muted">You can change it until '+nowLabel(can.deadline)+'.</span></div>'; }
else h+='<div class="notice info">'+esc(can.reason)+'</div>';
h+='<div class="row"><button class="btn" data-act="neworder">Start a new order</button></div></section>';
h+='<section class="panel"><span class="label">Scheduled reminders</span><div class="timeline" style="margin-top:8px">';
if(!rem.length) h+='<p class="muted small">No reminders: the slot is too soon, or there are no contact details for the enabled channels.</p>';
rem.forEach(function(r){ h+='<div><span class="mono small">'+nowLabel(r.sendAt)+'</span><span><span class="pill confirmed">'+r.channel+'</span> '+esc(r.body)+'</span></div>'; });
h+='</div><p class="small muted">Reminders are planned again automatically whenever the booking is rescheduled.</p></section></div>';
if(S.resched) h+=renderResched(b);
return h;
}
function renderResched(b){
var loc=locById(b.locationId);
var av=E.computeAvailability({location:loc,method:b.method,cart:b.cart||{items:[]},rules:RULES,peakDays:PEAKS,usage:store.usage(now()),settings:SETTINGS,now:now()});
var opts=''; av.dates.forEach(function(d){ if(d.available) opts+='<option value="'+d.date+'"'+(S.resched.date===d.date?' selected':'')+'>'+dLabel(d.date)+'</option>'; });
if(!S.resched.date){ var f=av.dates.filter(function(d){return d.available;})[0]; S.resched.date=f&&f.date; }
var d=av.dates.filter(function(x){return x.date===S.resched.date;})[0];
var sl=''; (d?d.slots:[]).forEach(function(s){ if(s.available) sl+='<option value="'+s.id+'">'+slotLabel(s.start,s.end)+' · '+(s.remaining==null?'open':s.remaining+' left')+'</option>'; });
return '<section class="panel" style="margin-top:18px"><h3>Change your '+b.method+' time</h3><p class="small muted">Same rules as checkout: only dates your items can make are shown.</p><div class="row"><select id="rsDate">'+opts+'</select><select id="rsSlot">'+sl+'</select><button class="btn primary" data-act="resched-save">Save new time</button><button class="btn" data-act="resched-cancel">Cancel</button></div></section>';
}
/* ---------- admin ---------- */
var TABS=[["overview","Today"],["bookings","Bookings"],["peak","Rush control"],["fresh","Products & freshness"],["run","Driver run sheet"],["diag","Why unavailable?"],["settings","Reminders & policy"]];
function renderAdmin(){
var review=Array.from(store.bookings.values()).filter(function(b){return b.status==="needs_review";}).length;
var h='<div class="admin"><nav class="nav" aria-label="Admin sections">';
TABS.forEach(function(t){ h+='<button data-act="tab" data-v="'+t[0]+'" aria-current="'+(S.tab===t[0])+'"><span>'+t[1]+'</span>'+(t[0]==="bookings"&&review?'<span class="badge">'+review+'</span>':'')+'</button>'; });
h+='</nav><div class="stack">'+({overview:adminOverview,bookings:adminBookings,peak:adminPeak,fresh:adminFresh,run:adminRun,diag:adminDiag,settings:adminSettings}[S.tab])()+'</div></div>';
return h;
}
function allBookings(){ return Array.from(store.bookings.values()); }
function adminOverview(){
var today=E.localParts(now(),TZ).date, wk=E.addDays(today,7);
var bs=allBookings();
var tdy=bs.filter(function(b){return b.date===today&&b.status==="confirmed";}).length;
var next=bs.filter(function(b){return b.date>today&&b.date<=wk&&b.status==="confirmed";}).length;
var rev=bs.filter(function(b){return b.status==="needs_review";}).length;
var wl=store.waitlist.filter(function(w){return !w.notifiedAt;}).length;
var h='<div class="sectionhead"><h2>Today, '+dLabel(today,{weekday:"long",month:"long"})+'</h2><span class="small muted">Kitchener — Guelph St</span></div>';
h+='<div class="kpis"><div class="kpi"><div class="label">Bookings today</div><div class="v num">'+tdy+'</div></div><div class="kpi"><div class="label">Next 7 days</div><div class="v num">'+next+'</div></div><div class="kpi'+(rev?' alert':'')+'"><div class="label">Needs review</div><div class="v num">'+rev+'</div></div><div class="kpi"><div class="label">On waitlist</div><div class="v num">'+wl+'</div></div></div>';
var u=store.usage(now()); var loc=LOCS[0];
h+='<section class="panel"><div class="sectionhead"><h3>Delivery capacity, next 28 days</h3><div class="legend" style="margin:0"><span><i style="background:var(--accent)"></i>Booked</span><span><i style="background:var(--peak)"></i>Festival day</span><span><i style="background:var(--bad)"></i>Full</span></div></div><div class="capchart" role="img" aria-label="Booked delivery orders against daily capacity per day">';
for(var i=0;i<28;i++){
var d=E.addDays(today,i); var hours=E.hoursFor(loc,"delivery",d).hours;
var caps=E.capsFor(loc,"delivery",d,"10:00-13:00",PEAKS); var used=(u["kit|delivery|"+d]||{day:0}).day;
var cap=caps.dayCap||12; var pct=hours.length?Math.min(100,Math.round(100*used/cap)):0;
var pk=E.peakFor(PEAKS,"kit","delivery",d);
h+='<div class="cbar'+(pk?' peak':'')+(hours.length&&used>=cap?' full':'')+'" title="'+dLabel(d)+': '+used+' of '+cap+'"><div class="track">'+(hours.length?'<div class="fill" style="height:'+pct+'%"></div>':'')+'</div><span>'+(hours.length?used+'/'+cap:'—')+'</span><span>'+d.slice(8)+'</span></div>';
}
h+='</div></section>';
var pk=PEAKS[0]; var pd=pk.dates[pk.dates.length-1];
var used8=(u["kit|delivery|"+pd]||{day:0}).day;
var deadline=E.zonedToUtc(E.addDays(pd,-pk.cutoff.daysBefore),pk.cutoff.time,TZ);
h+='<section class="panel"><div class="sectionhead"><h3>Next festival: '+esc(pk.name)+'</h3><button class="btn sm" data-act="tab" data-v="peak">Adjust rush settings</button></div><p>'+dLabel(pd,{weekday:"long",month:"long"})+' delivery: <b class="num">'+used8+' of '+pk.dailyCapacity+'</b> booked. '+(now()<deadline?'Orders close '+nowLabel(deadline)+'.':'Orders closed '+nowLabel(deadline)+'.')+' '+store.waitlist.length+' customers on the waitlist.</p></section>';
return h;
}
function adminBookings(){
var f=S.bf; var today=E.localParts(now(),TZ).date;
var rows=allBookings().filter(function(b){
if(f.status!=="all"&&b.status!==f.status) return false;
if(f.method!=="all"&&b.method!==f.method) return false;
if(f.loc!=="all"&&b.locationId!==f.loc) return false;
if(f.from&&b.date&&b.date<f.from) return false;
if(!f.from&&f.status==="all"&&b.date&&b.date<today) return false;
return true;
}).sort(function(a,b){ if(a.status!==b.status){ if(a.status==="needs_review") return -1; if(b.status==="needs_review") return 1; } return (a.date+(a.slotStart||"")).localeCompare(b.date+(b.slotStart||"")); });
var h='<div class="sectionhead"><h2>Bookings</h2><button class="btn sm" data-act="csv">Copy filtered CSV</button></div>';
h+='<div class="filters"><div class="field"><label class="label" for="fStatus">Status</label><select id="fStatus">'+opt(["all","confirmed","needs_review","cancelled"],f.status)+'</select></div><div class="field"><label class="label" for="fMethod">Method</label><select id="fMethod">'+opt(["all","delivery","pickup","shipping"],f.method)+'</select></div><div class="field"><label class="label" for="fLoc">Location</label><select id="fLoc"><option value="all">all</option>'+LOCS.map(function(l){return '<option value="'+l.id+'"'+(l.id===f.loc?' selected':'')+'>'+esc(l.name)+'</option>';}).join("")+'</select></div><div class="field"><label class="label" for="fFrom">From date</label><input type="date" id="fFrom" value="'+f.from+'"></div><span class="small muted">'+rows.length+' bookings</span></div>';
h+='<div class="tablewrap"><table><thead><tr><th>Order</th><th>Date</th><th>Time</th><th>Method</th><th>Customer</th><th>Status</th><th>Action</th></tr></thead><tbody>';
rows.slice(0,80).forEach(function(b){
h+='<tr><td class="mono">'+esc(b.orderName)+'</td><td>'+(b.date?dLabel(b.date):'<span class="muted">missing</span>')+'</td><td class="mono">'+(b.slotStart?slotLabel(b.slotStart,b.slotEnd):'—')+'</td><td>'+b.method+(b.peakDayId?' <span class="pill peak">Diwali</span>':'')+'</td><td>'+esc(b.customer&&b.customer.name)+'</td><td><span class="pill '+b.status+'">'+b.status.replace("_"," ")+'</span>'+(b.note?'<div class="small muted" style="white-space:normal;max-width:240px">'+esc(b.note)+'</div>':'')+'</td><td>'+(b.status==="confirmed"?'<button class="btn sm" data-act="admin-move" data-v="'+b.id+'">Move</button>':b.status==="needs_review"?'<button class="btn sm" data-act="admin-move" data-v="'+b.id+'">Pick a time</button>':'')+'</td></tr>';
if(S.moving===b.id) h+='<tr><td colspan="7" class="wrap">'+adminMoveForm(b)+'</td></tr>';
});
h+='</tbody></table></div><textarea id="csvOut" aria-label="CSV export" hidden></textarea>';
return h;
}
function adminMoveForm(b){
var loc=locById(b.locationId||"kit"); var method=b.method||"pickup";
var av=E.computeAvailability({location:loc,method:method,cart:{items:[]},rules:[],peakDays:PEAKS,usage:store.usage(now()),settings:SETTINGS,now:now()});
var o=''; var cnt=0;
av.dates.forEach(function(d){ d.slots.forEach(function(s){ if(s.available&&cnt<60){ cnt++; o+='<option value="'+d.date+'|'+s.id+'">'+dLabel(d.date)+' · '+(av.dateOnly?'dispatch':slotLabel(s.start,s.end))+' · '+(s.remaining==null?'open':s.remaining+' left')+'</option>'; } }); });
return '<div class="row"><span class="small">Move '+esc(b.orderName)+' to</span><select id="mvSel">'+o+'</select><button class="btn primary sm" data-act="admin-move-save" data-v="'+b.id+'">Save</button><button class="btn sm" data-act="admin-move-cancel">Cancel</button><span class="small muted">The old place is released only if the new one saves.</span></div>';
}
function opt(list,v){ return list.map(function(x){return '<option'+(x===v?' selected':'')+'>'+x+'</option>';}).join(""); }
function adminPeak(){
var p=PEAKS[0];
var h='<div class="sectionhead"><h2>Rush control</h2><span class="small muted">Changes apply to the customer view immediately</span></div>';
h+='<section class="panel stack"><div class="sectionhead"><h3>'+esc(p.name)+'</h3><span class="mono small">'+p.dates.map(function(d){return dLabel(d);}).join(", ")+'</span></div><div class="form">'+
num("pkDay","Daily order limit",p.dailyCapacity)+num("pkSlot","Per-slot limit",p.slotCapacity)+num("pkPrep","Extra prep (minutes)",p.extraPrepMinutes)+num("pkDays","Close orders N days before",p.cutoff.daysBefore)+
'<div class="field"><label class="label" for="pkTime">Cutoff time</label><input type="time" id="pkTime" value="'+p.cutoff.time+'"></div>'+num("pkSur","Peak surcharge (cents)",p.surcharge)+
'<div class="field"><span class="label">When full</span><label class="switch"><input type="checkbox" id="pkWait"'+(p.waitlist?' checked':'')+'> Offer a waitlist</label></div></div>';
h+='<div class="field"><label class="label" for="pkMsg">Customer banner</label><input type="text" id="pkMsg" value="'+esc(p.message)+'"></div><div><button class="btn primary" data-act="peak-save">Save rush settings</button></div></section>';
h+='<section class="panel"><h3>Waitlist</h3><div class="tablewrap" style="margin-top:8px"><table><thead><tr><th>Joined</th><th>Customer</th><th>Wants</th><th>Status</th></tr></thead><tbody>';
store.waitlist.forEach(function(w){ h+='<tr><td class="mono small">'+nowLabel(w.createdAt)+'</td><td>'+esc(w.contact.name||w.contact.email||w.contact.phone)+'</td><td>'+dLabel(w.date)+' · '+w.method+' · '+(w.slot==="*"?"any time":slotLabel(w.slot.split("-")[0],w.slot.split("-")[1]))+'</td><td>'+(w.notifiedAt?'<span class="pill confirmed">Offered a place</span>':'<span class="pill peak">Waiting</span>')+'</td></tr>'; });
h+='</tbody></table></div><p class="small muted">When an order on a full day is cancelled or moved, the first person waiting is offered the place automatically.</p></section>';
return h;
}
function num(id,label,v){ return '<div class="field"><label class="label" for="'+id+'">'+label+'</label><input type="number" id="'+id+'" value="'+(v==null?"":v)+'"></div>'; }
function adminFresh(){
var fr=RULES.filter(function(r){return r.id==="r_fresh";})[0];
var today=E.localParts(now(),TZ).date;
var h='<div class="sectionhead"><h2>Products & freshness</h2></div><section class="panel"><h3>Rules</h3><div class="tablewrap" style="margin-top:8px"><table><thead><tr><th>On</th><th>Rule</th><th>Applies to</th><th>Effect</th></tr></thead><tbody>';
RULES.forEach(function(r){ h+='<tr><td><input type="checkbox" aria-label="Enable '+esc(r.name)+'" data-act="rule-toggle" data-v="'+r.id+'"'+(r.enabled?' checked':'')+'></td><td>'+esc(r.name)+'</td><td><code>'+r.target.type+': '+esc(r.target.values.join(", "))+'</code></td><td class="wrap">'+esc(describeAction(r.action))+'</td></tr>'; });
h+='</tbody></table></div><p class="small muted">If rules disagree, a prohibition beats an allowance, and a mixed cart uses its longest preparation time. Customers see which item caused a restriction.</p></section>';
var a=fr.action;
h+='<section class="panel stack"><div class="sectionhead"><h3>Fresh Marigold Garland: stock arrivals</h3><span class="small muted">Sellable from arrival + '+a.minDaysAfterArrival+' day to arrival + '+a.shelfLifeDays+' days</span></div><div class="chips">';
a.arrivals.forEach(function(x,i){ h+='<span class="chip">'+dLabel(x.date)+'<button aria-label="Remove arrival '+x.date+'" data-act="arr-del" data-v="'+i+'">×</button></span>'; });
h+='</div><div class="row"><input type="date" id="arrNew"><button class="btn sm" data-act="arr-add">Add arrival</button>'+'<label class="small">Ready after <input type="number" id="frMin" value="'+a.minDaysAfterArrival+'" style="width:60px"> day(s)</label><label class="small">Shelf life <input type="number" id="frShelf" value="'+a.shelfLifeDays+'" style="width:60px"> days</label><button class="btn sm" data-act="fr-save">Update</button></div>';
h+='<div class="label">Dates a garland can be delivered, next 28 days</div><div class="strip">';
var cons=E.evaluateRules({items:[Object.assign({},PRODUCTS[0],{quantity:1})]},RULES);
for(var i=0;i<28;i++){ var d=E.addDays(today,i); var ok=!E.checkFreshness(cons.freshness,d,"kit"); h+='<div class="'+(ok?'y':'n')+'" title="'+dLabel(d)+'">'+d.slice(8)+'</div>'; }
h+='</div></section>';
return h;
}
function describeAction(a){
switch(a.type){ case "prohibitMethods": return "Blocks "+a.methods.join(", "); case "allowOnlyMethods": return "Only "+a.methods.join(", ");
case "addPrep": return "Adds "+fmtMins(a.minutes)+" preparation"; case "freshness": return a.arrivals.length+" stock arrivals; sellable for "+(a.shelfLifeDays-a.minDaysAfterArrival+1)+" days each";
case "restrictLocations": return "Only from "+a.locationIds.join(", "); case "blockWeekdays": return "Blocks weekdays "+a.weekdays.join(","); default: return a.type; }
}
function adminRun(){
var today=E.localParts(now(),TZ).date;
var dates=[]; allBookings().forEach(function(b){ if(b.method==="delivery"&&b.status==="confirmed"&&b.date>=today&&dates.indexOf(b.date)<0) dates.push(b.date); }); dates.sort();
if(!S.run.date||dates.indexOf(S.run.date)<0) S.run.date=dates[0];
var sheet=E.buildRunSheet(allBookings(),LOCS[0],S.run.date);
var h='<div class="sectionhead"><h2>Driver run sheet</h2><div class="row"><select id="runDate" aria-label="Run date">'+dates.slice(0,20).map(function(d){return '<option value="'+d+'"'+(d===S.run.date?' selected':'')+'>'+dLabel(d)+'</option>';}).join("")+'</select><button class="btn sm" data-act="run-csv">Copy CSV</button></div></div>';
h+='<div class="kpis"><div class="kpi"><div class="label">Stops</div><div class="v num">'+sheet.stops.length+'</div></div><div class="kpi"><div class="label">Est. distance</div><div class="v num">'+(sheet.totalKm||0)+' km</div></div><div class="kpi"><div class="label">Windows</div><div class="v num">'+new Set(sheet.stops.map(function(s){return s.window;})).size+'</div></div><div class="kpi"><div class="label">Missing address</div><div class="v num">'+sheet.unlocated.length+'</div></div></div>';
h+='<section class="panel"><div class="sectionhead"><h3>Route links</h3><span class="small muted">Promised windows first, nearest stop next. Straight-line estimate.</span></div><div class="row" style="margin-top:8px">';
sheet.mapsUrls.forEach(function(u,i){ h+='<a class="btn sm" href="'+esc(u)+'" target="_blank" rel="noopener">Open leg '+(i+1)+' in Google Maps</a>'; });
h+='</div></section><div class="tablewrap"><table><thead><tr><th>#</th><th>Window</th><th>Order</th><th>Customer</th><th>Address</th><th>Leg</th><th>Items</th></tr></thead><tbody>';
sheet.stops.forEach(function(s){ var b=s.booking; h+='<tr><td class="num">'+s.seq+'</td><td class="mono">'+slotLabel(b.slotStart,b.slotEnd)+'</td><td class="mono">'+esc(b.orderName)+'</td><td>'+esc(b.customer&&b.customer.name)+'</td><td>'+esc(b.address&&b.address.line1)+'</td><td class="num">'+(s.legKm!=null?s.legKm.toFixed(1)+' km':'—')+'</td><td>'+esc((b.items||[]).map(function(i){return i.quantity+"× "+i.title;}).join(", "))+'</td></tr>'; });
h+='</tbody></table></div><textarea id="csvOut" aria-label="CSV export" hidden></textarea>';
return h;
}
function adminDiag(){
var today=E.localParts(now(),TZ).date; var D=S.diag; if(!D.date) D.date=E.addDays(today,5);
var loc=locById(D.loc); var c=cart();
var av=E.computeAvailability({location:loc,method:D.method,cart:c,rules:RULES,peakDays:PEAKS,usage:store.usage(now()),settings:SETTINGS,now:now()});
var cons=E.evaluateRules(c,RULES,SETTINGS.mixedPrepPolicy);
var h='<div class="sectionhead"><h2>Why is a date unavailable?</h2><span class="small muted">Uses the cart from the customer view</span></div>';
h+='<div class="filters"><div class="field"><label class="label" for="dgM">Method</label><select id="dgM">'+opt(["delivery","pickup","shipping"],D.method)+'</select></div><div class="field"><label class="label" for="dgL">Location</label><select id="dgL">'+LOCS.map(function(l){return '<option value="'+l.id+'"'+(l.id===D.loc?' selected':'')+'>'+esc(l.name)+'</option>';}).join("")+'</select></div><div class="field"><label class="label" for="dgD">Date</label><input type="date" id="dgD" value="'+D.date+'"></div></div>';
h+='<section class="panel stack"><div><span class="label">Cart</span><p style="margin:4px 0">'+esc(c.items.map(function(i){return i.quantity+"× "+i.title;}).join(", ")||"Empty")+'</p><p class="small muted" style="margin:0">Matched rules: '+(cons.matched.length?esc(cons.matched.map(function(m){return m.ruleName+" ("+m.itemTitle+")";}).join("; ")):"none")+'. Preparation used: '+fmtMins(av.prepMinutes)+'.</p></div><div class="reasons">';
if(av.blockers.length) av.blockers.forEach(function(r){ h+='<div class="reason"><code>'+r.code+'</code><span>'+esc(r.message)+(r.ruleId?' · rule <code>'+r.ruleId+'</code>':'')+'</span></div>'; });
else { var d=av.dates.filter(function(x){return x.date===D.date;})[0];
if(!d) h+='<div class="reason"><code>BEYOND_HORIZON</code><span>Outside the '+loc.methods[D.method].horizonDays+'-day booking window, or before today.</span></div>';
else { d.reasons.forEach(function(r){ h+='<div class="reason"><code>'+r.code+'</code><span>'+esc(r.message)+(r.ruleId?' · rule <code>'+r.ruleId+'</code>':'')+'</span></div>'; });
d.slots.forEach(function(s){ h+='<div class="reason'+(s.available?' ok':'')+'"><span class="mono">'+(av.dateOnly?'Dispatch':slotLabel(s.start,s.end))+'</span><span>'+(s.available?'Available'+(s.remaining!=null?', '+s.remaining+' left':''):s.reasons.map(function(r){return r.code+": "+r.message;}).join("; "))+'</span></div>'; }); } }
h+='</div></section>';
return h;
}
function adminSettings(){
var R=SETTINGS.reschedule;
var h='<div class="sectionhead"><h2>Reminders & policy</h2></div><section class="panel stack"><h3>Customer self-reschedule</h3><div class="form"><div class="field"><span class="label">Allowed</span><label class="switch"><input type="checkbox" id="rsOn"'+(R.enabled?' checked':'')+'> Customers can change their time</label></div>'+num("rsHrs","Up to N hours before",R.minHoursBefore)+num("rsMax","Max changes per order",R.maxChanges)+num("holdMin","Hold a picked slot (minutes)",SETTINGS.holdMinutes)+'</div><div><button class="btn primary" data-act="pol-save">Save policy</button></div></section>';
h+='<section class="panel"><h3>Reminders</h3><div class="tablewrap" style="margin-top:8px"><table><thead><tr><th>On</th><th>Channel</th><th>When</th><th>Methods</th><th>Message</th></tr></thead><tbody>';
SETTINGS.reminders.forEach(function(r){ h+='<tr><td><input type="checkbox" data-act="rem-toggle" data-v="'+r.id+'" aria-label="Enable reminder"'+(r.enabled?' checked':'')+'></td><td>'+r.channel+'</td><td>'+fmtMins(r.minutesBefore)+' before</td><td>'+(r.methods?r.methods.join(", "):"all")+'</td><td class="wrap small">'+esc(r.template)+'</td></tr>'; });
h+='</tbody></table></div><p class="small muted">WhatsApp uses the Meta Cloud API with approved templates; SMS uses Twilio; email uses your sending domain. Customers who opt out are skipped.</p></section>';
return h;
}
/* ---------- render + events ---------- */
var app=document.getElementById("app");
function render(){
document.getElementById("v-shop").setAttribute("aria-pressed",S.view==="shop");
document.getElementById("v-admin").setAttribute("aria-pressed",S.view==="admin");
document.getElementById("nowLabel").textContent=nowLabel(now());
app.innerHTML=S.view==="shop"?renderShop():renderAdmin();
tick();
}
function tick(){
document.getElementById("nowLabel").textContent=nowLabel(now());
var el=document.getElementById("holdLeft"); var h=myHold();
if(el&&h){ var s=Math.max(0,Math.round((h.expiresAt-now())/1000)); el.textContent=Math.floor(s/60)+":"+String(s%60).padStart(2,"0"); }
if(el&&!h&&S.view==="shop") render();
}
setInterval(tick,1000);
function holdSlot(date,slot){
var sh=computeShop(); if(!sh.chosen) return;
if(!S.token) S.token="tok_"+Math.random().toString(36).slice(2);
var sel={locationId:sh.chosen.location.id,method:S.method,date:date,slot:slot};
var v=E.validateSelection(ctx({location:sh.chosen.location}),date,slot);
if(!v.ok){ S.notice={kind:"bad",text:v.reasons[0].message}; S.sel=null; return; }
var r=store.hold(S.token,sel,E.capsFor(sh.chosen.location,S.method,date,slot,PEAKS),now(),SETTINGS.holdMinutes);
if(r.ok){ S.sel=sel; S.notice=null; } else { S.notice={kind:"bad",text:r.message+". Please choose another time."}; S.sel=null; }
}
function val(id){ var e=document.getElementById(id); return e?e.value:null; }
document.addEventListener("click",function(e){
var t=e.target.closest("[data-act]"); if(!t||t.tagName==="SELECT"||t.type==="checkbox") return;
var a=t.dataset.act, v=t.dataset.v;
if(a==="view"){ S.view=v; }
else if(a==="tab"){ S.view="admin"; S.tab=v; S.moving=null; }
else if(a==="skip"){ setClock(now()+11*60000); }
else if(a==="qty"){ S.cart[v]=Math.max(0,(S.cart[v]||0)+(+t.dataset.d)); }
else if(a==="method"){ S.method=v; S.date=null; }
else if(a==="loc"){ S.locId=v; S.date=null; }
else if(a==="postal"){ S.postal=val("postal"); S.date=null; }
else if(a==="month"){ var p=S.month.split("-"); var m=+p[1]+(+t.dataset.d); var y=+p[0]; if(m<1){m=12;y--;} if(m>12){m=1;y++;} S.month=y+"-"+String(m).padStart(2,"0"); }
else if(a==="date"){ S.date=v; S.notice=null; }
else if(a==="slot"){ holdSlot(S.date,v); }
else if(a==="rush"){ if(S.sel){ var sh=computeShop(); var caps=E.capsFor(sh.chosen.location,S.sel.method,S.sel.date,S.sel.slot,PEAKS); for(var i=0;i<40;i++){ var r=store.hold("rush_"+Date.now()+"_"+i,S.sel,caps,now(),SETTINGS.holdMinutes); if(!r.ok) break; } S.notice={kind:"info",text:"Other shoppers took every remaining place in this slot. "+(myHold()?"Yours is still held.":"")}; } }
else if(a==="waitlist"){ var em=val("waitEmail"); var sh2=computeShop(); if(em&&sh2.chosen){ store.joinWaitlist({locationId:sh2.chosen.location.id,method:S.method,date:S.date,slot:"*"},{email:em,name:em},now()); S.notice={kind:"ok",text:"You're on the waitlist for "+dLabel(S.date)+". We'll message "+em+" if a place opens."}; S.waitEmail=""; } }
else if(a==="checkout"){
S.customer={name:val("cName"),email:val("cEmail"),phone:val("cPhone")};
var sh3=computeShop(); var c=cart(); var loc=sh3.chosen.location; var caps2=E.capsFor(loc,S.sel.method,S.sel.date,S.sel.slot,PEAKS);
var g=geocode(S.postal); var fee=feeQuote(sh3);
var res=store.confirm("o"+(++orderNo),S.token,S.sel,caps2,now(),{orderName:"#"+orderNo,timezone:TZ,customer:S.customer,address:S.method==="delivery"?{line1:"Customer address",postal:S.postal,lat:g.lat,lng:g.lng}:undefined,items:c.items.map(function(i){return {title:i.title,quantity:i.quantity};}),fee:fee?fee.fee:0,peakDayId:(E.peakFor(PEAKS,loc.id,S.sel.method,S.sel.date)||{}).id||null});
res.booking.cart=c;
S.lastOrder=res.booking.id; S.sel=null; S.token=null; S.date=null;
S.notice=res.ok?(res.reReserved?{kind:"info",text:"Your hold had expired, but the place was still free, so the booking went through."}:null):{kind:"bad",text:res.message+". The order is in the merchant's review queue."};
}
else if(a==="neworder"){ S.lastOrder=null; S.notice=null; }
else if(a==="resched-open"){ S.resched={date:null}; }
else if(a==="resched-cancel"){ S.resched=null; }
else if(a==="resched-save"){ var b=store.bookings.get(S.lastOrder); var d=val("rsDate"), s=val("rsSlot"); if(d&&s){ var r2=store.reschedule(b.id,{locationId:b.locationId,method:b.method,date:d,slot:s},E.capsFor(locById(b.locationId),b.method,d,s,PEAKS),now(),"customer"); S.notice=r2.ok?{kind:"ok",text:"Changed to "+dLabel(d)+", "+slotLabel(s.split("-")[0],s.split("-")[1])+". Reminders were re-planned."}:{kind:"bad",text:r2.message}; offerWaitlist(r2); S.resched=null; } }
else if(a==="admin-move"){ S.moving=v; }
else if(a==="admin-move-cancel"){ S.moving=null; }
else if(a==="admin-move-save"){ var bk=store.bookings.get(v); var pv=val("mvSel"); if(pv){ var p2=pv.split("|"); var loc2=locById(bk.locationId||"kit");
if(bk.status==="needs_review"){ bk.status="confirmed"; bk.locationId=loc2.id; bk.method=bk.method||"pickup"; var caps3=E.capsFor(loc2,bk.method,p2[0],p2[1],PEAKS); var u=store.usage(now())[loc2.id+"|"+bk.method+"|"+p2[0]]||{day:0,slots:{}}; if((caps3.dayCap!=null&&u.day>=caps3.dayCap)||(caps3.slotCap!=null&&(u.slots[p2[1]]||0)>=caps3.slotCap)){ bk.status="needs_review"; S.flash="That slot just filled."; } else { bk.date=p2[0]; bk.slotStart=p2[1].split("-")[0]; bk.slotEnd=p2[1].split("-")[1]; bk.note="Resolved by merchant"; store.audit.push({at:now(),actor:"merchant",bookingId:bk.id,action:"resolved"}); } }
else { var r3=store.reschedule(bk.id,{locationId:bk.locationId,method:bk.method,date:p2[0],slot:p2[1]},E.capsFor(loc2,bk.method,p2[0],p2[1],PEAKS),now(),"merchant"); offerWaitlist(r3); }
S.moving=null; } }
else if(a==="csv"||a==="run-csv"){ var ta=document.getElementById("csvOut"); var csv=a==="csv"?bookingsCsv():runCsv(); ta.hidden=false; ta.value=csv; try{ navigator.clipboard.writeText(csv).then(function(){t.textContent="Copied";},function(){ta.select();}); }catch(_){ ta.select(); } return; }
else if(a==="peak-save"){ var p=PEAKS[0]; p.dailyCapacity=+val("pkDay")||null; p.slotCapacity=+val("pkSlot")||null; p.extraPrepMinutes=+val("pkPrep")||0; p.cutoff={daysBefore:+val("pkDays")||0,time:val("pkTime")||"18:00"}; p.surcharge=+val("pkSur")||0; p.waitlist=document.getElementById("pkWait").checked; p.message=val("pkMsg"); t.textContent="Saved"; return; }
else if(a==="arr-add"){ var nd=val("arrNew"); if(nd){ RULES[3].action.arrivals.push({date:nd}); RULES[3].action.arrivals.sort(function(x,y){return x.date<y.date?-1:1;}); } }
else if(a==="arr-del"){ RULES[3].action.arrivals.splice(+v,1); }
else if(a==="fr-save"){ RULES[3].action.minDaysAfterArrival=+val("frMin")||0; RULES[3].action.shelfLifeDays=+val("frShelf")||1; }
else if(a==="pol-save"){ SETTINGS.reschedule.enabled=document.getElementById("rsOn").checked; SETTINGS.reschedule.minHoursBefore=+val("rsHrs")||0; SETTINGS.reschedule.maxChanges=+val("rsMax")||0; SETTINGS.holdMinutes=+val("holdMin")||10; t.textContent="Saved"; return; }
else return;
render();
});
function offerWaitlist(r){
if(!r||!r.ok||!r.freed) return;
var w=store.nextWaitlisted(r.freed)||store.nextWaitlisted(Object.assign({},r.freed,{slot:"*"}));
if(w){ w.notifiedAt=now(); }
}
document.addEventListener("change",function(e){
var t=e.target, id=t.id;
if(t.dataset.act==="rule-toggle"){ RULES.forEach(function(r){ if(r.id===t.dataset.v) r.enabled=t.checked; }); render(); return; }
if(t.dataset.act==="rem-toggle"){ SETTINGS.reminders.forEach(function(r){ if(r.id===t.dataset.v) r.enabled=t.checked; }); return; }
if(id==="preset"){ if(t.value==="live") setClock(Date.now()); else { var p=t.value.split("T"); setClock(E.zonedToUtc(p[0],p[1],TZ)); } S.month=null; S.date=null; if(S.token) store.release(S.token); S.sel=null; S.notice=null; render(); return; }
if(id==="fStatus"){ S.bf.status=t.value; } else if(id==="fMethod"){ S.bf.method=t.value; } else if(id==="fLoc"){ S.bf.loc=t.value; } else if(id==="fFrom"){ S.bf.from=t.value; }
else if(id==="runDate"){ S.run.date=t.value; }
else if(id==="dgM"){ S.diag.method=t.value; } else if(id==="dgL"){ S.diag.loc=t.value; } else if(id==="dgD"){ S.diag.date=t.value; }
else if(id==="rsDate"){ S.resched.date=t.value; }
else if(id==="postal"){ S.postal=t.value; S.date=null; }
else return;
render();
});
document.addEventListener("keydown",function(e){ if(e.key==="Enter"&&e.target.id==="postal"){ S.postal=e.target.value; S.date=null; render(); } });
function bookingsCsv(){
var rows=allBookings().map(function(b){ var loc=locById(b.locationId); return {order:b.orderName,status:b.status,method:b.method,location:loc?loc.name:"",date:b.date,slot_start:b.slotStart||"",slot_end:b.slotEnd||"",timezone:b.timezone,start_utc:b.date&&b.slotStart?new Date(E.zonedToUtc(b.date,b.slotStart,b.timezone)).toISOString():"",customer:b.customer&&b.customer.name,email:b.customer&&b.customer.email,items:(b.items||[]).map(function(i){return i.quantity+"x "+i.title;}).join("; ")}; });
return E.toCsv(rows);
}
function runCsv(){
var sheet=E.buildRunSheet(allBookings(),LOCS[0],S.run.date);
return E.toCsv(sheet.stops.map(function(s){ var b=s.booking; return {stop:s.seq,window:b.slotStart+"-"+b.slotEnd,order:b.orderName,customer:b.customer&&b.customer.name,phone:b.customer&&b.customer.phone,address:b.address&&b.address.line1,lat:b.address.lat.toFixed(5),lng:b.address.lng.toFixed(5),leg_km:s.legKm!=null?s.legKm.toFixed(1):""}; }));
}
setClock(E.zonedToUtc("2026-11-05","10:00",TZ));
document.getElementById("preset").value="2026-11-05T10:00";
render();
})();
</script>